mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-17 20:19:20 +00:00
Compare commits
97 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a5164d170 | ||
|
|
028228ee12 | ||
|
|
4e27174c85 | ||
|
|
d65be69c4c | ||
|
|
5ec75fb6df | ||
|
|
cbbc6af128 | ||
|
|
7320440b61 | ||
|
|
637343f751 | ||
|
|
ffb6d65910 | ||
|
|
cff9e47162 | ||
|
|
02ceee6669 | ||
|
|
4bd1a5f955 | ||
|
|
6ad75e72e6 | ||
|
|
c696370cff | ||
|
|
5a807af505 | ||
|
|
4fe7988249 | ||
|
|
58de9913b9 | ||
|
|
b52ceceeb7 | ||
|
|
8990c48e7b | ||
|
|
18bd330a59 | ||
|
|
b02604396d | ||
|
|
7d844411fb | ||
|
|
fba28d6941 | ||
|
|
57f7933a4b | ||
|
|
2045306283 | ||
|
|
4fb479e2f5 | ||
|
|
77dba4834c | ||
|
|
a70be4299e | ||
|
|
4ed86dbff2 | ||
|
|
63f6970b45 | ||
|
|
7802f6c528 | ||
|
|
7fc8150617 | ||
|
|
6e92c78cbd | ||
|
|
5beea361fe | ||
|
|
aa5e16e681 | ||
|
|
27f454d07e | ||
|
|
bf8a2d0e3a | ||
|
|
d890391531 | ||
|
|
6e1d9a3dbc | ||
|
|
703be50bc7 | ||
|
|
013d0ff99c | ||
|
|
b1b157aa19 | ||
|
|
e7e5727015 | ||
|
|
ad3a5b58e4 | ||
|
|
f501c8b336 | ||
|
|
f7706211ea | ||
|
|
a551660d2a | ||
|
|
5cbe170117 | ||
|
|
f55b561ff9 | ||
|
|
753c1aa406 | ||
|
|
ff85f2a940 | ||
|
|
04c605d76b | ||
|
|
3fabca1051 | ||
|
|
32ec73dd96 | ||
|
|
f30bede2e0 | ||
|
|
1dcb065c64 | ||
|
|
feb452b740 | ||
|
|
86b276a3ec | ||
|
|
0d8a52f63b | ||
|
|
2bd96713db | ||
|
|
1d2edec550 | ||
|
|
965126df83 | ||
|
|
6e2cde4a21 | ||
|
|
a75081b8c0 | ||
|
|
b68e1f6ea6 | ||
|
|
9239e735ad | ||
|
|
576ff49943 | ||
|
|
92b5c80a4a | ||
|
|
d188c8ec0e | ||
|
|
4055543f5d | ||
|
|
fc79c6bf54 | ||
|
|
522113ba98 | ||
|
|
6469526c26 | ||
|
|
122e6902ed | ||
|
|
64d93cec66 | ||
|
|
14ee13c77e | ||
|
|
b17178bba9 | ||
|
|
3845d4294c | ||
|
|
2fcfebe1d3 | ||
|
|
e205772482 | ||
|
|
7cb38a8f22 | ||
|
|
9d5c79d6b2 | ||
|
|
dd07ecad45 | ||
|
|
b0675f6490 | ||
|
|
4150ed9b3b | ||
|
|
0b2e2a2ab5 | ||
|
|
c70e6f23df | ||
|
|
5cf0c896bb | ||
|
|
d79e6e353e | ||
|
|
ce2bd15872 | ||
|
|
deef6ea66a | ||
|
|
aa66ee64ec | ||
|
|
eeb0567a73 | ||
|
|
c7dbcbcd17 | ||
|
|
4b76cc46a1 | ||
|
|
bfe2b923e1 | ||
|
|
dbdb971e10 |
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "15.5.0"
|
||||
__version__ = "15.7.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
},
|
||||
"Stocks": {
|
||||
"Mati\u00e8res premi\u00e8res": {},
|
||||
"Stock de produits fini": {},
|
||||
"Stock de produits fini": {
|
||||
"account_type": "Stock"
|
||||
},
|
||||
"Stock exp\u00e9di\u00e9 non-factur\u00e9": {},
|
||||
"Travaux en cours": {},
|
||||
"account_type": "Stock"
|
||||
@@ -395,9 +397,11 @@
|
||||
},
|
||||
"Produits": {
|
||||
"Revenus de ventes": {
|
||||
" Escomptes de volume sur ventes": {},
|
||||
"Escomptes de volume sur ventes": {},
|
||||
"Autres produits d'exploitation": {},
|
||||
"Ventes": {},
|
||||
"Ventes": {
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Ventes avec des provinces harmonis\u00e9es": {},
|
||||
"Ventes avec des provinces non-harmonis\u00e9es": {},
|
||||
"Ventes \u00e0 l'\u00e9tranger": {}
|
||||
|
||||
@@ -53,8 +53,13 @@
|
||||
},
|
||||
"II. Forderungen und sonstige Vermögensgegenstände": {
|
||||
"is_group": 1,
|
||||
"Ford. a. Lieferungen und Leistungen": {
|
||||
"Forderungen aus Lieferungen und Leistungen mit Kontokorrent": {
|
||||
"account_number": "1400",
|
||||
"account_type": "Receivable",
|
||||
"is_group": 1
|
||||
},
|
||||
"Forderungen aus Lieferungen und Leistungen ohne Kontokorrent": {
|
||||
"account_number": "1410",
|
||||
"account_type": "Receivable"
|
||||
},
|
||||
"Durchlaufende Posten": {
|
||||
@@ -180,8 +185,13 @@
|
||||
},
|
||||
"IV. Verbindlichkeiten aus Lieferungen und Leistungen": {
|
||||
"is_group": 1,
|
||||
"Verbindlichkeiten aus Lieferungen u. Leistungen": {
|
||||
"Verbindlichkeiten aus Lieferungen und Leistungen mit Kontokorrent": {
|
||||
"account_number": "1600",
|
||||
"account_type": "Payable",
|
||||
"is_group": 1
|
||||
},
|
||||
"Verbindlichkeiten aus Lieferungen und Leistungen ohne Kontokorrent": {
|
||||
"account_number": "1610",
|
||||
"account_type": "Payable"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -407,13 +407,10 @@
|
||||
"Bewertungskorrektur zu Forderungen aus Lieferungen und Leistungen": {
|
||||
"account_number": "9960"
|
||||
},
|
||||
"Debitoren": {
|
||||
"is_group": 1,
|
||||
"account_number": "10000"
|
||||
},
|
||||
"Forderungen aus Lieferungen und Leistungen": {
|
||||
"Forderungen aus Lieferungen und Leistungen mit Kontokorrent": {
|
||||
"account_number": "1200",
|
||||
"account_type": "Receivable"
|
||||
"account_type": "Receivable",
|
||||
"is_group": 1
|
||||
},
|
||||
"Forderungen aus Lieferungen und Leistungen ohne Kontokorrent": {
|
||||
"account_number": "1210"
|
||||
@@ -1138,18 +1135,15 @@
|
||||
"Bewertungskorrektur zu Verb. aus Lieferungen und Leistungen": {
|
||||
"account_number": "9964"
|
||||
},
|
||||
"Kreditoren": {
|
||||
"account_number": "70000",
|
||||
"Verb. aus Lieferungen und Leistungen mit Kontokorrent": {
|
||||
"account_number": "3300",
|
||||
"account_type": "Payable",
|
||||
"is_group": 1,
|
||||
"Wareneingangs-Verrechnungskonto" : {
|
||||
"Wareneingangs-Verrechnungskonto" : {
|
||||
"account_number": "70001",
|
||||
"account_type": "Stock Received But Not Billed"
|
||||
}
|
||||
},
|
||||
"Verb. aus Lieferungen und Leistungen": {
|
||||
"account_number": "3300",
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Verb. aus Lieferungen und Leistungen ohne Kontokorrent": {
|
||||
"account_number": "3310"
|
||||
},
|
||||
|
||||
@@ -18,6 +18,7 @@ from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_s
|
||||
get_entries,
|
||||
)
|
||||
from erpnext.accounts.utils import get_account_currency, get_balance_on
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
|
||||
class BankReconciliationTool(Document):
|
||||
@@ -150,7 +151,7 @@ def create_journal_entry_bts(
|
||||
bank_transaction = frappe.db.get_values(
|
||||
"Bank Transaction",
|
||||
bank_transaction_name,
|
||||
fieldname=["name", "deposit", "withdrawal", "bank_account"],
|
||||
fieldname=["name", "deposit", "withdrawal", "bank_account", "currency"],
|
||||
as_dict=True,
|
||||
)[0]
|
||||
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
|
||||
@@ -164,29 +165,94 @@ def create_journal_entry_bts(
|
||||
)
|
||||
|
||||
company = frappe.get_value("Account", company_account, "company")
|
||||
company_default_currency = frappe.get_cached_value("Company", company, "default_currency")
|
||||
company_account_currency = frappe.get_cached_value("Account", company_account, "account_currency")
|
||||
second_account_currency = frappe.get_cached_value("Account", second_account, "account_currency")
|
||||
|
||||
# determine if multi-currency Journal or not
|
||||
is_multi_currency = (
|
||||
True
|
||||
if company_default_currency != company_account_currency
|
||||
or company_default_currency != second_account_currency
|
||||
or company_default_currency != bank_transaction.currency
|
||||
else False
|
||||
)
|
||||
|
||||
accounts = []
|
||||
# Multi Currency?
|
||||
accounts.append(
|
||||
{
|
||||
"account": second_account,
|
||||
"credit_in_account_currency": bank_transaction.deposit,
|
||||
"debit_in_account_currency": bank_transaction.withdrawal,
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
"cost_center": get_default_cost_center(company),
|
||||
}
|
||||
)
|
||||
second_account_dict = {
|
||||
"account": second_account,
|
||||
"account_currency": second_account_currency,
|
||||
"credit_in_account_currency": bank_transaction.deposit,
|
||||
"debit_in_account_currency": bank_transaction.withdrawal,
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
"cost_center": get_default_cost_center(company),
|
||||
}
|
||||
|
||||
accounts.append(
|
||||
{
|
||||
"account": company_account,
|
||||
"bank_account": bank_transaction.bank_account,
|
||||
"credit_in_account_currency": bank_transaction.withdrawal,
|
||||
"debit_in_account_currency": bank_transaction.deposit,
|
||||
"cost_center": get_default_cost_center(company),
|
||||
}
|
||||
)
|
||||
company_account_dict = {
|
||||
"account": company_account,
|
||||
"account_currency": company_account_currency,
|
||||
"bank_account": bank_transaction.bank_account,
|
||||
"credit_in_account_currency": bank_transaction.withdrawal,
|
||||
"debit_in_account_currency": bank_transaction.deposit,
|
||||
"cost_center": get_default_cost_center(company),
|
||||
}
|
||||
|
||||
# convert transaction amount to company currency
|
||||
if is_multi_currency:
|
||||
exc_rate = get_exchange_rate(bank_transaction.currency, company_default_currency, posting_date)
|
||||
withdrawal_in_company_currency = flt(exc_rate * abs(bank_transaction.withdrawal))
|
||||
deposit_in_company_currency = flt(exc_rate * abs(bank_transaction.deposit))
|
||||
else:
|
||||
withdrawal_in_company_currency = bank_transaction.withdrawal
|
||||
deposit_in_company_currency = bank_transaction.deposit
|
||||
|
||||
# if second account is of foreign currency, convert and set debit and credit fields.
|
||||
if second_account_currency != company_default_currency:
|
||||
exc_rate = get_exchange_rate(second_account_currency, company_default_currency, posting_date)
|
||||
second_account_dict.update(
|
||||
{
|
||||
"exchange_rate": exc_rate,
|
||||
"credit": deposit_in_company_currency,
|
||||
"debit": withdrawal_in_company_currency,
|
||||
"credit_in_account_currency": flt(deposit_in_company_currency / exc_rate) or 0,
|
||||
"debit_in_account_currency": flt(withdrawal_in_company_currency / exc_rate) or 0,
|
||||
}
|
||||
)
|
||||
else:
|
||||
second_account_dict.update(
|
||||
{
|
||||
"exchange_rate": 1,
|
||||
"credit": deposit_in_company_currency,
|
||||
"debit": withdrawal_in_company_currency,
|
||||
"credit_in_account_currency": deposit_in_company_currency,
|
||||
"debit_in_account_currency": withdrawal_in_company_currency,
|
||||
}
|
||||
)
|
||||
|
||||
# if company account is of foreign currency, convert and set debit and credit fields.
|
||||
if company_account_currency != company_default_currency:
|
||||
exc_rate = get_exchange_rate(company_account_currency, company_default_currency, posting_date)
|
||||
company_account_dict.update(
|
||||
{
|
||||
"exchange_rate": exc_rate,
|
||||
"credit": withdrawal_in_company_currency,
|
||||
"debit": deposit_in_company_currency,
|
||||
}
|
||||
)
|
||||
else:
|
||||
company_account_dict.update(
|
||||
{
|
||||
"exchange_rate": 1,
|
||||
"credit": withdrawal_in_company_currency,
|
||||
"debit": deposit_in_company_currency,
|
||||
"credit_in_account_currency": withdrawal_in_company_currency,
|
||||
"debit_in_account_currency": deposit_in_company_currency,
|
||||
}
|
||||
)
|
||||
|
||||
accounts.append(second_account_dict)
|
||||
accounts.append(company_account_dict)
|
||||
|
||||
journal_entry_dict = {
|
||||
"voucher_type": entry_type,
|
||||
@@ -196,6 +262,9 @@ def create_journal_entry_bts(
|
||||
"cheque_no": reference_number,
|
||||
"mode_of_payment": mode_of_payment,
|
||||
}
|
||||
if is_multi_currency:
|
||||
journal_entry_dict.update({"multi_currency": True})
|
||||
|
||||
journal_entry = frappe.new_doc("Journal Entry")
|
||||
journal_entry.update(journal_entry_dict)
|
||||
journal_entry.set("accounts", accounts)
|
||||
|
||||
@@ -1689,13 +1689,43 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
return data
|
||||
|
||||
|
||||
def split_invoices_based_on_payment_terms(outstanding_invoices, company):
|
||||
invoice_ref_based_on_payment_terms = {}
|
||||
def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list:
|
||||
"""Split a list of invoices based on their payment terms."""
|
||||
exc_rates = get_currency_data(outstanding_invoices, company)
|
||||
|
||||
outstanding_invoices_after_split = []
|
||||
for entry in outstanding_invoices:
|
||||
if entry.voucher_type in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if payment_term_template := frappe.db.get_value(
|
||||
entry.voucher_type, entry.voucher_no, "payment_terms_template"
|
||||
):
|
||||
split_rows = get_split_invoice_rows(entry, payment_term_template, exc_rates)
|
||||
if not split_rows:
|
||||
continue
|
||||
|
||||
if len(split_rows) > 1:
|
||||
frappe.msgprint(
|
||||
_("Splitting {0} {1} into {2} rows as per Payment Terms").format(
|
||||
_(entry.voucher_type), frappe.bold(entry.voucher_no), len(split_rows)
|
||||
),
|
||||
alert=True,
|
||||
)
|
||||
outstanding_invoices_after_split += split_rows
|
||||
continue
|
||||
|
||||
# If not an invoice or no payment terms template, add as it is
|
||||
outstanding_invoices_after_split.append(entry)
|
||||
|
||||
return outstanding_invoices_after_split
|
||||
|
||||
|
||||
def get_currency_data(outstanding_invoices: list, company: str = None) -> dict:
|
||||
"""Get currency and conversion data for a list of invoices."""
|
||||
exc_rates = frappe._dict()
|
||||
company_currency = (
|
||||
frappe.db.get_value("Company", company, "default_currency") if company else None
|
||||
)
|
||||
exc_rates = frappe._dict()
|
||||
|
||||
for doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype]
|
||||
for x in frappe.db.get_all(
|
||||
@@ -1710,72 +1740,54 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company):
|
||||
company_currency=company_currency,
|
||||
)
|
||||
|
||||
for idx, d in enumerate(outstanding_invoices):
|
||||
if d.voucher_type in ["Sales Invoice", "Purchase Invoice"]:
|
||||
payment_term_template = frappe.db.get_value(
|
||||
d.voucher_type, d.voucher_no, "payment_terms_template"
|
||||
return exc_rates
|
||||
|
||||
|
||||
def get_split_invoice_rows(invoice: dict, payment_term_template: str, exc_rates: dict) -> list:
|
||||
"""Split invoice based on its payment schedule table."""
|
||||
split_rows = []
|
||||
allocate_payment_based_on_payment_terms = frappe.db.get_value(
|
||||
"Payment Terms Template", payment_term_template, "allocate_payment_based_on_payment_terms"
|
||||
)
|
||||
|
||||
if not allocate_payment_based_on_payment_terms:
|
||||
return [invoice]
|
||||
|
||||
payment_schedule = frappe.get_all(
|
||||
"Payment Schedule", filters={"parent": invoice.voucher_no}, fields=["*"], order_by="due_date"
|
||||
)
|
||||
for payment_term in payment_schedule:
|
||||
if not payment_term.outstanding > 0.1:
|
||||
continue
|
||||
|
||||
doc_details = exc_rates.get(payment_term.parent, None)
|
||||
is_multi_currency_acc = (doc_details.currency != doc_details.company_currency) and (
|
||||
doc_details.party_account_currency != doc_details.company_currency
|
||||
)
|
||||
payment_term_outstanding = flt(payment_term.outstanding)
|
||||
if not is_multi_currency_acc:
|
||||
payment_term_outstanding = doc_details.conversion_rate * flt(payment_term.outstanding)
|
||||
|
||||
split_rows.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"due_date": invoice.due_date,
|
||||
"currency": invoice.currency,
|
||||
"voucher_no": invoice.voucher_no,
|
||||
"voucher_type": invoice.voucher_type,
|
||||
"posting_date": invoice.posting_date,
|
||||
"invoice_amount": flt(invoice.invoice_amount),
|
||||
"outstanding_amount": payment_term_outstanding
|
||||
if payment_term_outstanding
|
||||
else invoice.outstanding_amount,
|
||||
"payment_term_outstanding": payment_term_outstanding,
|
||||
"payment_amount": payment_term.payment_amount,
|
||||
"payment_term": payment_term.payment_term,
|
||||
}
|
||||
)
|
||||
if payment_term_template:
|
||||
allocate_payment_based_on_payment_terms = frappe.get_cached_value(
|
||||
"Payment Terms Template", payment_term_template, "allocate_payment_based_on_payment_terms"
|
||||
)
|
||||
if allocate_payment_based_on_payment_terms:
|
||||
payment_schedule = frappe.get_all(
|
||||
"Payment Schedule", filters={"parent": d.voucher_no}, fields=["*"]
|
||||
)
|
||||
)
|
||||
|
||||
for payment_term in payment_schedule:
|
||||
if payment_term.outstanding > 0.1:
|
||||
doc_details = exc_rates.get(payment_term.parent, None)
|
||||
is_multi_currency_acc = (doc_details.currency != doc_details.company_currency) and (
|
||||
doc_details.party_account_currency != doc_details.company_currency
|
||||
)
|
||||
payment_term_outstanding = flt(payment_term.outstanding)
|
||||
if not is_multi_currency_acc:
|
||||
payment_term_outstanding = doc_details.conversion_rate * flt(payment_term.outstanding)
|
||||
|
||||
invoice_ref_based_on_payment_terms.setdefault(idx, [])
|
||||
invoice_ref_based_on_payment_terms[idx].append(
|
||||
frappe._dict(
|
||||
{
|
||||
"due_date": d.due_date,
|
||||
"currency": d.currency,
|
||||
"voucher_no": d.voucher_no,
|
||||
"voucher_type": d.voucher_type,
|
||||
"posting_date": d.posting_date,
|
||||
"invoice_amount": flt(d.invoice_amount),
|
||||
"outstanding_amount": payment_term_outstanding
|
||||
if payment_term_outstanding
|
||||
else d.outstanding_amount,
|
||||
"payment_term_outstanding": payment_term_outstanding,
|
||||
"payment_amount": payment_term.payment_amount,
|
||||
"payment_term": payment_term.payment_term,
|
||||
"account": d.account,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
outstanding_invoices_after_split = []
|
||||
if invoice_ref_based_on_payment_terms:
|
||||
for idx, ref in invoice_ref_based_on_payment_terms.items():
|
||||
voucher_no = ref[0]["voucher_no"]
|
||||
voucher_type = ref[0]["voucher_type"]
|
||||
|
||||
frappe.msgprint(
|
||||
_("Spliting {} {} into {} row(s) as per Payment Terms").format(
|
||||
voucher_type, voucher_no, len(ref)
|
||||
),
|
||||
alert=True,
|
||||
)
|
||||
|
||||
outstanding_invoices_after_split += invoice_ref_based_on_payment_terms[idx]
|
||||
|
||||
existing_row = list(filter(lambda x: x.get("voucher_no") == voucher_no, outstanding_invoices))
|
||||
index = outstanding_invoices.index(existing_row[0])
|
||||
outstanding_invoices.pop(index)
|
||||
|
||||
outstanding_invoices_after_split += outstanding_invoices
|
||||
return outstanding_invoices_after_split
|
||||
return split_rows
|
||||
|
||||
|
||||
def get_orders_to_be_billed(
|
||||
|
||||
@@ -6,11 +6,12 @@ import unittest
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import flt, nowdate
|
||||
from frappe.utils import add_days, flt, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
InvalidPaymentEntry,
|
||||
get_outstanding_reference_documents,
|
||||
get_payment_entry,
|
||||
get_reference_details,
|
||||
)
|
||||
@@ -1471,6 +1472,45 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
for field in ["account", "debit", "credit"]:
|
||||
self.assertEqual(self.expected_gle[row][field], gl_entries[row][field])
|
||||
|
||||
def test_outstanding_invoices_api(self):
|
||||
"""
|
||||
Test if `get_outstanding_reference_documents` fetches invoices in the right order.
|
||||
"""
|
||||
customer = create_customer("Max Mustermann", "INR")
|
||||
create_payment_terms_template()
|
||||
|
||||
# SI has an earlier due date and SI2 has a later due date
|
||||
si = create_sales_invoice(
|
||||
qty=1, rate=100, customer=customer, posting_date=add_days(nowdate(), -4)
|
||||
)
|
||||
si2 = create_sales_invoice(do_not_save=1, qty=1, rate=100, customer=customer)
|
||||
si2.payment_terms_template = "Test Receivable Template"
|
||||
si2.submit()
|
||||
|
||||
args = {
|
||||
"posting_date": nowdate(),
|
||||
"company": "_Test Company",
|
||||
"party_type": "Customer",
|
||||
"payment_type": "Pay",
|
||||
"party": customer,
|
||||
"party_account": "Debtors - _TC",
|
||||
}
|
||||
args.update(
|
||||
{
|
||||
"get_outstanding_invoices": True,
|
||||
"from_posting_date": add_days(nowdate(), -4),
|
||||
"to_posting_date": add_days(nowdate(), 2),
|
||||
}
|
||||
)
|
||||
references = get_outstanding_reference_documents(args)
|
||||
|
||||
self.assertEqual(len(references), 3)
|
||||
self.assertEqual(references[0].voucher_no, si.name)
|
||||
self.assertEqual(references[1].voucher_no, si2.name)
|
||||
self.assertEqual(references[2].voucher_no, si2.name)
|
||||
self.assertEqual(references[1].payment_term, "Basic Amount Receivable")
|
||||
self.assertEqual(references[2].payment_term, "Tax Receivable")
|
||||
|
||||
|
||||
def create_payment_entry(**args):
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
@@ -1531,6 +1571,9 @@ def create_payment_terms_template():
|
||||
def create_payment_terms_template_with_discount(
|
||||
name=None, discount_type=None, discount=None, template_name=None
|
||||
):
|
||||
"""
|
||||
Create a Payment Terms Template with % or amount discount.
|
||||
"""
|
||||
create_payment_term(name or "30 Credit Days with 10% Discount")
|
||||
template_name = template_name or "Test Discount Template"
|
||||
|
||||
|
||||
@@ -592,6 +592,27 @@ class PaymentReconciliation(Document):
|
||||
|
||||
invoice_exchange_map.update(purchase_invoice_map)
|
||||
|
||||
journals = [
|
||||
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Journal Entry"
|
||||
]
|
||||
journals.extend(
|
||||
[d.get("reference_name") for d in payments if d.get("reference_type") == "Journal Entry"]
|
||||
)
|
||||
if journals:
|
||||
journals = list(set(journals))
|
||||
journals_map = frappe._dict(
|
||||
frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={"parent": ("in", journals), "account": ("in", [self.receivable_payable_account])},
|
||||
fields=[
|
||||
"parent as `name`",
|
||||
"exchange_rate",
|
||||
],
|
||||
as_list=1,
|
||||
)
|
||||
)
|
||||
invoice_exchange_map.update(journals_map)
|
||||
|
||||
return invoice_exchange_map
|
||||
|
||||
def validate_allocation(self):
|
||||
|
||||
@@ -34,4 +34,6 @@ class PaymentReconciliationAllocation(Document):
|
||||
unreconciled_amount: DF.Currency
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@staticmethod
|
||||
def get_list(args):
|
||||
pass
|
||||
|
||||
@@ -26,4 +26,6 @@ class PaymentReconciliationInvoice(Document):
|
||||
parenttype: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@staticmethod
|
||||
def get_list(args):
|
||||
pass
|
||||
|
||||
@@ -30,4 +30,6 @@ class PaymentReconciliationPayment(Document):
|
||||
remark: DF.SmallText | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@staticmethod
|
||||
def get_list(args):
|
||||
pass
|
||||
|
||||
@@ -581,6 +581,8 @@ def apply_pricing_rule_on_transaction(doc):
|
||||
if d.price_or_product_discount == "Price":
|
||||
if d.apply_discount_on:
|
||||
doc.set("apply_discount_on", d.apply_discount_on)
|
||||
# Variable to track whether the condition has been met
|
||||
condition_met = False
|
||||
|
||||
for field in ["additional_discount_percentage", "discount_amount"]:
|
||||
pr_field = "discount_percentage" if field == "additional_discount_percentage" else field
|
||||
@@ -603,6 +605,11 @@ def apply_pricing_rule_on_transaction(doc):
|
||||
if coupon_code_pricing_rule == d.name:
|
||||
# if selected coupon code is linked with pricing rule
|
||||
doc.set(field, d.get(pr_field))
|
||||
|
||||
# Set the condition_met variable to True and break out of the loop
|
||||
condition_met = True
|
||||
break
|
||||
|
||||
else:
|
||||
# reset discount if not linked
|
||||
doc.set(field, 0)
|
||||
@@ -611,6 +618,10 @@ def apply_pricing_rule_on_transaction(doc):
|
||||
doc.set(field, 0)
|
||||
|
||||
doc.calculate_taxes_and_totals()
|
||||
|
||||
# Break out of the main loop if the condition is met
|
||||
if condition_met:
|
||||
break
|
||||
elif d.price_or_product_discount == "Product":
|
||||
item_details = frappe._dict({"parenttype": doc.doctype, "free_item_data": []})
|
||||
get_product_discount_rule(d, item_details, doc=doc)
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"group_by",
|
||||
"cost_center",
|
||||
"territory",
|
||||
"ignore_exchange_rate_revaluation_journals",
|
||||
"column_break_14",
|
||||
"to_date",
|
||||
"finance_book",
|
||||
@@ -376,10 +377,16 @@
|
||||
"fieldname": "pdf_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "PDF Name"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ignore_exchange_rate_revaluation_journals",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Exchange Rate Revaluation Journals"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2023-08-28 12:59:53.071334",
|
||||
"modified": "2023-12-18 12:20:08.965120",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
||||
@@ -56,6 +56,7 @@ class ProcessStatementOfAccounts(Document):
|
||||
frequency: DF.Literal["Weekly", "Monthly", "Quarterly"]
|
||||
from_date: DF.Date | None
|
||||
group_by: DF.Literal["", "Group by Voucher", "Group by Voucher (Consolidated)"]
|
||||
ignore_exchange_rate_revaluation_journals: DF.Check
|
||||
include_ageing: DF.Check
|
||||
include_break: DF.Check
|
||||
letter_head: DF.Link | None
|
||||
@@ -119,6 +120,18 @@ def get_statement_dict(doc, get_statement_dict=False):
|
||||
statement_dict = {}
|
||||
ageing = ""
|
||||
|
||||
err_journals = None
|
||||
if doc.report == "General Ledger" and doc.ignore_exchange_rate_revaluation_journals:
|
||||
err_journals = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"company": doc.company,
|
||||
"docstatus": 1,
|
||||
"voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]),
|
||||
},
|
||||
as_list=True,
|
||||
)
|
||||
|
||||
for entry in doc.customers:
|
||||
if doc.include_ageing:
|
||||
ageing = set_ageing(doc, entry)
|
||||
@@ -131,6 +144,8 @@ def get_statement_dict(doc, get_statement_dict=False):
|
||||
)
|
||||
|
||||
filters = get_common_filters(doc)
|
||||
if err_journals:
|
||||
filters.update({"voucher_no_not_in": [x[0] for x in err_journals]})
|
||||
|
||||
if doc.report == "General Ledger":
|
||||
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
|
||||
|
||||
@@ -126,7 +126,7 @@ class RepostAccountingLedger(Document):
|
||||
return rendered_page
|
||||
|
||||
def on_submit(self):
|
||||
if len(self.vouchers) > 1:
|
||||
if len(self.vouchers) > 5:
|
||||
job_name = "repost_accounting_ledger_" + self.name
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost",
|
||||
@@ -170,8 +170,6 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
doc.make_gl_entries(1)
|
||||
doc.make_gl_entries()
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
def get_allowed_types_from_settings():
|
||||
return [
|
||||
|
||||
@@ -20,18 +20,11 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.update_repost_settings()
|
||||
update_repost_settings()
|
||||
|
||||
def teadDown(self):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def update_repost_settings(self):
|
||||
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
|
||||
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
|
||||
for x in allowed_types:
|
||||
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
|
||||
repost_settings.save()
|
||||
|
||||
def test_01_basic_functions(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
@@ -90,9 +83,6 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
# Submit repost document
|
||||
ral.save().submit()
|
||||
|
||||
# background jobs don't run on test cases. Manually triggering repost function.
|
||||
start_repost(ral.name)
|
||||
|
||||
res = (
|
||||
qb.from_(gl)
|
||||
.select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit"))
|
||||
@@ -177,26 +167,6 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.save().submit()
|
||||
|
||||
# without deletion flag set
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.delete_cancelled_entries = False
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
ral.save()
|
||||
|
||||
# assert preview data is generated
|
||||
preview = ral.generate_preview()
|
||||
self.assertIsNotNone(preview)
|
||||
|
||||
ral.save().submit()
|
||||
|
||||
# background jobs don't run on test cases. Manually triggering repost function.
|
||||
start_repost(ral.name)
|
||||
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
||||
|
||||
# with deletion flag set
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
@@ -205,6 +175,38 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
ral.save().submit()
|
||||
|
||||
start_repost(ral.name)
|
||||
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
||||
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
||||
|
||||
def test_05_without_deletion_flag(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.save().submit()
|
||||
|
||||
# without deletion flag set
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.delete_cancelled_entries = False
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
ral.save().submit()
|
||||
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
||||
|
||||
|
||||
def update_repost_settings():
|
||||
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
|
||||
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
|
||||
for x in allowed_types:
|
||||
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
|
||||
repost_settings.save()
|
||||
|
||||
@@ -2356,9 +2356,18 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
|
||||
|
||||
def get_received_items(reference_name, doctype, reference_fieldname):
|
||||
reference_field = "inter_company_invoice_reference"
|
||||
if doctype == "Purchase Order":
|
||||
reference_field = "inter_company_order_reference"
|
||||
|
||||
filters = {
|
||||
reference_field: reference_name,
|
||||
"docstatus": 1,
|
||||
}
|
||||
|
||||
target_doctypes = frappe.get_all(
|
||||
doctype,
|
||||
filters={"inter_company_invoice_reference": reference_name, "docstatus": 1},
|
||||
filters=filters,
|
||||
as_list=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -2799,6 +2799,12 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
@change_settings("Selling Settings", {"enable_discount_accounting": 1})
|
||||
def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self):
|
||||
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.test_repost_accounting_ledger import (
|
||||
update_repost_settings,
|
||||
)
|
||||
|
||||
update_repost_settings()
|
||||
|
||||
additional_discount_account = create_account(
|
||||
account_name="Discount Account",
|
||||
parent_account="Indirect Expenses - _TC",
|
||||
|
||||
@@ -242,8 +242,12 @@ class ReceivablePayableReport(object):
|
||||
row.invoiced_in_account_currency += amount_in_account_currency
|
||||
else:
|
||||
if self.is_invoice(ple):
|
||||
row.credit_note -= amount
|
||||
row.credit_note_in_account_currency -= amount_in_account_currency
|
||||
if row.voucher_no == ple.voucher_no == ple.against_voucher_no:
|
||||
row.paid -= amount
|
||||
row.paid_in_account_currency -= amount_in_account_currency
|
||||
else:
|
||||
row.credit_note -= amount
|
||||
row.credit_note_in_account_currency -= amount_in_account_currency
|
||||
else:
|
||||
row.paid -= amount
|
||||
row.paid_in_account_currency -= amount_in_account_currency
|
||||
|
||||
@@ -76,6 +76,41 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
|
||||
return credit_note
|
||||
|
||||
def test_pos_receivable(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"party_type": "Customer",
|
||||
"party": [self.customer],
|
||||
"report_date": add_days(today(), 2),
|
||||
"based_on_payment_terms": 0,
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"show_remarks": False,
|
||||
}
|
||||
|
||||
pos_inv = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
pos_inv.posting_date = add_days(today(), 2)
|
||||
pos_inv.is_pos = 1
|
||||
pos_inv.append(
|
||||
"payments",
|
||||
frappe._dict(
|
||||
mode_of_payment="Cash",
|
||||
amount=flt(pos_inv.grand_total / 2),
|
||||
),
|
||||
)
|
||||
pos_inv.disable_rounded_total = 1
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
report = execute(filters)
|
||||
expected_data = [[pos_inv.grand_total, pos_inv.paid_amount, 0]]
|
||||
|
||||
row = report[1][-1]
|
||||
self.assertEqual(expected_data[0], [row.invoiced, row.paid, row.credit_note])
|
||||
pos_inv.cancel()
|
||||
|
||||
def test_accounts_receivable(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
|
||||
@@ -8,7 +8,17 @@ import re
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
cint,
|
||||
cstr,
|
||||
flt,
|
||||
formatdate,
|
||||
get_first_day,
|
||||
getdate,
|
||||
today,
|
||||
)
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
@@ -43,6 +53,8 @@ def get_period_list(
|
||||
year_start_date = getdate(period_start_date)
|
||||
year_end_date = getdate(period_end_date)
|
||||
|
||||
year_end_date = getdate(today()) if year_end_date > getdate(today()) else year_end_date
|
||||
|
||||
months_to_add = {"Yearly": 12, "Half-Yearly": 6, "Quarterly": 3, "Monthly": 1}[periodicity]
|
||||
|
||||
period_list = []
|
||||
|
||||
@@ -238,6 +238,9 @@ def get_conditions(filters):
|
||||
if filters.get("voucher_no"):
|
||||
conditions.append("voucher_no=%(voucher_no)s")
|
||||
|
||||
if filters.get("voucher_no_not_in"):
|
||||
conditions.append("voucher_no not in %(voucher_no_not_in)s")
|
||||
|
||||
if filters.get("group_by") == "Group by Party" and not filters.get("party_type"):
|
||||
conditions.append("party_type in ('Customer', 'Supplier')")
|
||||
|
||||
|
||||
@@ -89,6 +89,8 @@ def _execute(filters=None, additional_table_columns=None):
|
||||
"payable_account": inv.credit_to,
|
||||
"mode_of_payment": inv.mode_of_payment,
|
||||
"project": ", ".join(project) if inv.doctype == "Purchase Invoice" else inv.project,
|
||||
"bill_no": inv.bill_no,
|
||||
"bill_date": inv.bill_date,
|
||||
"remarks": inv.remarks,
|
||||
"purchase_order": ", ".join(purchase_order),
|
||||
"purchase_receipt": ", ".join(purchase_receipt),
|
||||
|
||||
@@ -345,21 +345,16 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts):
|
||||
|
||||
if filters.get("party"):
|
||||
party = [filters.get("party")]
|
||||
query = query.where(
|
||||
((gle.account.isin(tds_accounts) & gle.against.isin(party)))
|
||||
| ((gle.voucher_type == "Journal Entry") & (gle.party == filters.get("party")))
|
||||
| gle.party.isin(party)
|
||||
jv_condition = gle.against.isin(party) | (
|
||||
(gle.voucher_type == "Journal Entry") & (gle.party == filters.get("party"))
|
||||
)
|
||||
else:
|
||||
party = frappe.get_all(filters.get("party_type"), pluck="name")
|
||||
query = query.where(
|
||||
((gle.account.isin(tds_accounts) & gle.against.isin(party)))
|
||||
| (
|
||||
(gle.voucher_type == "Journal Entry")
|
||||
& ((gle.party_type == filters.get("party_type")) | (gle.party_type == ""))
|
||||
)
|
||||
| gle.party.isin(party)
|
||||
jv_condition = gle.against.isin(party) | (
|
||||
(gle.voucher_type == "Journal Entry")
|
||||
& ((gle.party_type == filters.get("party_type")) | (gle.party_type == ""))
|
||||
)
|
||||
query = query.where((gle.account.isin(tds_accounts) & jv_condition) | gle.party.isin(party))
|
||||
return query
|
||||
|
||||
|
||||
|
||||
@@ -657,8 +657,10 @@ def update_reference_in_payment_entry(
|
||||
"total_amount": d.grand_total,
|
||||
"outstanding_amount": d.outstanding_amount,
|
||||
"allocated_amount": d.allocated_amount,
|
||||
"exchange_rate": d.exchange_rate if d.exchange_gain_loss else payment_entry.get_exchange_rate(),
|
||||
"exchange_gain_loss": d.exchange_gain_loss,
|
||||
"exchange_rate": d.exchange_rate
|
||||
if d.difference_amount is not None
|
||||
else payment_entry.get_exchange_rate(),
|
||||
"exchange_gain_loss": d.difference_amount,
|
||||
"account": d.account,
|
||||
}
|
||||
|
||||
|
||||
@@ -340,6 +340,10 @@ class AssetDepreciationSchedule(Document):
|
||||
n == 0
|
||||
and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
|
||||
and not self.opening_accumulated_depreciation
|
||||
and get_updated_rate_of_depreciation_for_wdv_and_dd(
|
||||
asset_doc, value_after_depreciation, row, False
|
||||
)
|
||||
== row.rate_of_depreciation
|
||||
):
|
||||
from_date = add_days(
|
||||
asset_doc.available_for_use_date, -1
|
||||
@@ -605,7 +609,9 @@ def get_depreciation_amount(
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def get_updated_rate_of_depreciation_for_wdv_and_dd(asset, depreciable_value, fb_row):
|
||||
def get_updated_rate_of_depreciation_for_wdv_and_dd(
|
||||
asset, depreciable_value, fb_row, show_msg=True
|
||||
):
|
||||
return fb_row.rate_of_depreciation
|
||||
|
||||
|
||||
|
||||
@@ -214,7 +214,7 @@ frappe.ui.form.on("Purchase Order Item", {
|
||||
}
|
||||
},
|
||||
|
||||
fg_item_qty: async function(frm, cdt, cdn) {
|
||||
qty: async function (frm, cdt, cdn) {
|
||||
if (frm.doc.is_subcontracted && !frm.doc.is_old_subcontracting_flow) {
|
||||
var row = locals[cdt][cdn];
|
||||
|
||||
@@ -222,7 +222,7 @@ frappe.ui.form.on("Purchase Order Item", {
|
||||
var result = await frm.events.get_subcontracting_boms_for_finished_goods(row.fg_item)
|
||||
|
||||
if (result.message && row.item_code == result.message.service_item && row.uom == result.message.service_item_uom) {
|
||||
frappe.model.set_value(cdt, cdn, "qty", flt(row.fg_item_qty) * flt(result.message.conversion_factor));
|
||||
frappe.model.set_value(cdt, cdn, "fg_item_qty", flt(row.qty) / flt(result.message.conversion_factor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,15 @@ class RequestforQuotation(BuyingController):
|
||||
supplier.quote_status = "Pending"
|
||||
self.send_to_supplier()
|
||||
|
||||
def before_print(self, settings=None):
|
||||
"""Use the first suppliers data to render the print preview."""
|
||||
if self.vendor or not self.suppliers:
|
||||
# If a specific supplier is already set, via Tools > Download PDF,
|
||||
# we don't want to override it.
|
||||
return
|
||||
|
||||
self.update_supplier_part_no(self.suppliers[0].supplier)
|
||||
|
||||
def on_cancel(self):
|
||||
self.db_set("status", "Cancelled")
|
||||
|
||||
|
||||
@@ -166,6 +166,7 @@ class AccountsController(TransactionBase):
|
||||
self.disable_pricing_rule_on_internal_transfer()
|
||||
self.disable_tax_included_prices_for_internal_transfer()
|
||||
self.set_incoming_rate()
|
||||
self.init_internal_values()
|
||||
|
||||
if self.meta.get_field("currency"):
|
||||
self.calculate_taxes_and_totals()
|
||||
@@ -225,6 +226,16 @@ class AccountsController(TransactionBase):
|
||||
|
||||
self.set_total_in_words()
|
||||
|
||||
def init_internal_values(self):
|
||||
# init all the internal values as 0 on sa
|
||||
if self.docstatus.is_draft():
|
||||
# TODO: Add all such pending values here
|
||||
fields = ["billed_amt", "delivered_qty"]
|
||||
for item in self.get("items"):
|
||||
for field in fields:
|
||||
if hasattr(item, field):
|
||||
item.set(field, 0)
|
||||
|
||||
def before_cancel(self):
|
||||
validate_einvoice_fields(self)
|
||||
|
||||
@@ -292,6 +303,7 @@ class AccountsController(TransactionBase):
|
||||
def on_trash(self):
|
||||
self._remove_references_in_repost_doctypes()
|
||||
self._remove_references_in_unreconcile()
|
||||
self.remove_serial_and_batch_bundle()
|
||||
|
||||
# delete sl and gl entries on deletion of transaction
|
||||
if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"):
|
||||
@@ -307,6 +319,15 @@ class AccountsController(TransactionBase):
|
||||
(self.doctype, self.name),
|
||||
)
|
||||
|
||||
def remove_serial_and_batch_bundle(self):
|
||||
bundles = frappe.get_all(
|
||||
"Serial and Batch Bundle",
|
||||
filters={"voucher_type": self.doctype, "voucher_no": self.name, "docstatus": ("!=", 1)},
|
||||
)
|
||||
|
||||
for bundle in bundles:
|
||||
frappe.delete_doc("Serial and Batch Bundle", bundle.name)
|
||||
|
||||
def validate_deferred_income_expense_account(self):
|
||||
field_map = {
|
||||
"Sales Invoice": "deferred_revenue_account",
|
||||
@@ -629,6 +650,7 @@ class AccountsController(TransactionBase):
|
||||
|
||||
args["doctype"] = self.doctype
|
||||
args["name"] = self.name
|
||||
args["child_doctype"] = item.doctype
|
||||
args["child_docname"] = item.name
|
||||
args["ignore_pricing_rule"] = (
|
||||
self.ignore_pricing_rule if hasattr(self, "ignore_pricing_rule") else 0
|
||||
|
||||
@@ -381,7 +381,11 @@ class BuyingController(SubcontractingController):
|
||||
|
||||
rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
|
||||
else:
|
||||
field = "incoming_rate" if self.get("is_internal_supplier") else "rate"
|
||||
field = (
|
||||
"incoming_rate"
|
||||
if self.get("is_internal_supplier") and not self.doctype == "Purchase Order"
|
||||
else "rate"
|
||||
)
|
||||
rate = flt(
|
||||
frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
|
||||
* (d.conversion_factor or 1),
|
||||
|
||||
@@ -56,10 +56,24 @@ def make_variant_based_on_manufacturer(template, manufacturer, manufacturer_part
|
||||
|
||||
copy_attributes_to_variant(template, variant)
|
||||
|
||||
variant.manufacturer = manufacturer
|
||||
variant.manufacturer_part_no = manufacturer_part_no
|
||||
|
||||
variant.item_code = append_number_if_name_exists("Item", template.name)
|
||||
variant.flags.ignore_mandatory = True
|
||||
variant.save()
|
||||
|
||||
if not frappe.db.exists(
|
||||
"Item Manufacturer", {"item_code": variant.name, "manufacturer": manufacturer}
|
||||
):
|
||||
manufacturer_doc = frappe.new_doc("Item Manufacturer")
|
||||
manufacturer_doc.update(
|
||||
{
|
||||
"item_code": variant.name,
|
||||
"manufacturer": manufacturer,
|
||||
"manufacturer_part_no": manufacturer_part_no,
|
||||
}
|
||||
)
|
||||
|
||||
manufacturer_doc.flags.ignore_mandatory = True
|
||||
manufacturer_doc.save(ignore_permissions=True)
|
||||
|
||||
return variant
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ from frappe.model.meta import get_field_precision
|
||||
from frappe.utils import flt, format_datetime, get_datetime
|
||||
|
||||
import erpnext
|
||||
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
|
||||
from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
|
||||
|
||||
@@ -69,8 +71,6 @@ def validate_return_against(doc):
|
||||
|
||||
|
||||
def validate_returned_items(doc):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
valid_items = frappe._dict()
|
||||
|
||||
select_fields = "item_code, qty, stock_qty, rate, parenttype, conversion_factor"
|
||||
@@ -123,26 +123,6 @@ def validate_returned_items(doc):
|
||||
)
|
||||
)
|
||||
|
||||
elif ref.batch_no and d.batch_no not in ref.batch_no:
|
||||
frappe.throw(
|
||||
_("Row # {0}: Batch No must be same as {1} {2}").format(
|
||||
d.idx, doc.doctype, doc.return_against
|
||||
)
|
||||
)
|
||||
|
||||
elif ref.serial_no:
|
||||
if d.qty and not d.serial_no:
|
||||
frappe.throw(_("Row # {0}: Serial No is mandatory").format(d.idx))
|
||||
else:
|
||||
serial_nos = get_serial_nos(d.serial_no)
|
||||
for s in serial_nos:
|
||||
if s not in ref.serial_no:
|
||||
frappe.throw(
|
||||
_("Row # {0}: Serial No {1} does not match with {2} {3}").format(
|
||||
d.idx, s, doc.doctype, doc.return_against
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
warehouse_mandatory
|
||||
and not d.get("warehouse")
|
||||
@@ -397,71 +377,92 @@ def make_return_doc(
|
||||
else:
|
||||
doc.run_method("calculate_taxes_and_totals")
|
||||
|
||||
def update_item(source_doc, target_doc, source_parent):
|
||||
def update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||
|
||||
target_doc.qty = -1 * source_doc.qty
|
||||
item_details = frappe.get_cached_value(
|
||||
"Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
|
||||
)
|
||||
|
||||
returned_serial_nos = []
|
||||
if source_doc.get("serial_and_batch_bundle"):
|
||||
if item_details.has_serial_no:
|
||||
returned_serial_nos = get_returned_serial_nos(source_doc, source_parent)
|
||||
returned_batches = frappe._dict()
|
||||
serial_and_batch_field = (
|
||||
"serial_and_batch_bundle" if qty_field == "stock_qty" else "rejected_serial_and_batch_bundle"
|
||||
)
|
||||
old_serial_no_field = "serial_no" if qty_field == "stock_qty" else "rejected_serial_no"
|
||||
old_batch_no_field = "batch_no"
|
||||
|
||||
type_of_transaction = "Inward"
|
||||
if (
|
||||
frappe.db.get_value(
|
||||
"Serial and Batch Bundle", source_doc.serial_and_batch_bundle, "type_of_transaction"
|
||||
)
|
||||
== "Inward"
|
||||
):
|
||||
type_of_transaction = "Outward"
|
||||
|
||||
cls_obj = SerialBatchCreation(
|
||||
{
|
||||
"type_of_transaction": type_of_transaction,
|
||||
"serial_and_batch_bundle": source_doc.serial_and_batch_bundle,
|
||||
"returned_against": source_doc.name,
|
||||
"item_code": source_doc.item_code,
|
||||
"returned_serial_nos": returned_serial_nos,
|
||||
}
|
||||
)
|
||||
|
||||
cls_obj.duplicate_package()
|
||||
if cls_obj.serial_and_batch_bundle:
|
||||
target_doc.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle
|
||||
|
||||
if source_doc.get("rejected_serial_and_batch_bundle"):
|
||||
if (
|
||||
source_doc.get(serial_and_batch_field)
|
||||
or source_doc.get(old_serial_no_field)
|
||||
or source_doc.get(old_batch_no_field)
|
||||
):
|
||||
if item_details.has_serial_no:
|
||||
returned_serial_nos = get_returned_serial_nos(
|
||||
source_doc, source_parent, serial_no_field="rejected_serial_and_batch_bundle"
|
||||
source_doc, source_parent, serial_no_field=serial_and_batch_field
|
||||
)
|
||||
else:
|
||||
returned_batches = get_returned_batches(
|
||||
source_doc, source_parent, batch_no_field=serial_and_batch_field
|
||||
)
|
||||
|
||||
type_of_transaction = "Inward"
|
||||
if (
|
||||
if source_doc.get(serial_and_batch_field) and (
|
||||
frappe.db.get_value(
|
||||
"Serial and Batch Bundle", source_doc.rejected_serial_and_batch_bundle, "type_of_transaction"
|
||||
"Serial and Batch Bundle", source_doc.get(serial_and_batch_field), "type_of_transaction"
|
||||
)
|
||||
== "Inward"
|
||||
):
|
||||
type_of_transaction = "Outward"
|
||||
elif source_parent.doctype in [
|
||||
"Purchase Invoice",
|
||||
"Purchase Receipt",
|
||||
"Subcontracting Receipt",
|
||||
]:
|
||||
type_of_transaction = "Outward"
|
||||
|
||||
cls_obj = SerialBatchCreation(
|
||||
{
|
||||
"type_of_transaction": type_of_transaction,
|
||||
"serial_and_batch_bundle": source_doc.rejected_serial_and_batch_bundle,
|
||||
"serial_and_batch_bundle": source_doc.get(serial_and_batch_field),
|
||||
"returned_against": source_doc.name,
|
||||
"item_code": source_doc.item_code,
|
||||
"returned_serial_nos": returned_serial_nos,
|
||||
"voucher_type": source_parent.doctype,
|
||||
"do_not_submit": True,
|
||||
"warehouse": source_doc.warehouse,
|
||||
"has_serial_no": item_details.has_serial_no,
|
||||
"has_batch_no": item_details.has_batch_no,
|
||||
}
|
||||
)
|
||||
|
||||
cls_obj.duplicate_package()
|
||||
if cls_obj.serial_and_batch_bundle:
|
||||
target_doc.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle
|
||||
serial_nos = []
|
||||
batches = frappe._dict()
|
||||
if source_doc.get(old_batch_no_field):
|
||||
batches = frappe._dict({source_doc.batch_no: source_doc.get(qty_field)})
|
||||
elif source_doc.get(old_serial_no_field):
|
||||
serial_nos = get_serial_nos(source_doc.get(old_serial_no_field))
|
||||
elif source_doc.get(serial_and_batch_field):
|
||||
if item_details.has_serial_no:
|
||||
serial_nos = get_serial_nos_from_bundle(source_doc.get(serial_and_batch_field))
|
||||
else:
|
||||
batches = get_batches_from_bundle(source_doc.get(serial_and_batch_field))
|
||||
|
||||
if serial_nos:
|
||||
cls_obj.serial_nos = sorted(list(set(serial_nos) - set(returned_serial_nos)))
|
||||
elif batches:
|
||||
for batch in batches:
|
||||
if batch in returned_batches:
|
||||
batches[batch] -= flt(returned_batches.get(batch))
|
||||
|
||||
cls_obj.batches = batches
|
||||
|
||||
if source_doc.get(serial_and_batch_field):
|
||||
cls_obj.duplicate_package()
|
||||
if cls_obj.serial_and_batch_bundle:
|
||||
target_doc.set(serial_and_batch_field, cls_obj.serial_and_batch_bundle)
|
||||
else:
|
||||
target_doc.set(serial_and_batch_field, cls_obj.make_serial_and_batch_bundle().name)
|
||||
|
||||
def update_item(source_doc, target_doc, source_parent):
|
||||
target_doc.qty = -1 * source_doc.qty
|
||||
if doctype in ["Purchase Receipt", "Subcontracting Receipt"]:
|
||||
returned_qty_map = get_returned_qty_map_for_row(
|
||||
source_parent.name, source_parent.supplier, source_doc.name, doctype
|
||||
@@ -561,6 +562,17 @@ def make_return_doc(
|
||||
if default_warehouse_for_sales_return:
|
||||
target_doc.warehouse = default_warehouse_for_sales_return
|
||||
|
||||
item_details = frappe.get_cached_value(
|
||||
"Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
|
||||
)
|
||||
|
||||
if not item_details.has_batch_no and not item_details.has_serial_no:
|
||||
return
|
||||
|
||||
for qty_field in ["stock_qty", "rejected_qty"]:
|
||||
if target_doc.get(qty_field):
|
||||
update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field)
|
||||
|
||||
def update_terms(source_doc, target_doc, source_parent):
|
||||
target_doc.payment_amount = -source_doc.payment_amount
|
||||
|
||||
@@ -716,6 +728,9 @@ def get_returned_serial_nos(
|
||||
[parent_doc.doctype, "docstatus", "=", 1],
|
||||
]
|
||||
|
||||
if serial_no_field == "rejected_serial_and_batch_bundle":
|
||||
filters.append([child_doc.doctype, "rejected_qty", ">", 0])
|
||||
|
||||
# Required for POS Invoice
|
||||
if ignore_voucher_detail_no:
|
||||
filters.append([child_doc.doctype, "name", "!=", ignore_voucher_detail_no])
|
||||
@@ -723,9 +738,57 @@ def get_returned_serial_nos(
|
||||
ids = []
|
||||
for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters):
|
||||
ids.append(row.get("serial_and_batch_bundle"))
|
||||
if row.get(old_field):
|
||||
if row.get(old_field) and not row.get(serial_no_field):
|
||||
serial_nos.extend(get_serial_nos_from_serial_no(row.get(old_field)))
|
||||
|
||||
serial_nos.extend(get_serial_nos(ids))
|
||||
if ids:
|
||||
serial_nos.extend(get_serial_nos(ids))
|
||||
|
||||
return serial_nos
|
||||
|
||||
|
||||
def get_returned_batches(
|
||||
child_doc, parent_doc, batch_no_field=None, ignore_voucher_detail_no=None
|
||||
):
|
||||
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
|
||||
|
||||
batches = frappe._dict()
|
||||
|
||||
old_field = "batch_no"
|
||||
if not batch_no_field:
|
||||
batch_no_field = "serial_and_batch_bundle"
|
||||
|
||||
return_ref_field = frappe.scrub(child_doc.doctype)
|
||||
if child_doc.doctype == "Delivery Note Item":
|
||||
return_ref_field = "dn_detail"
|
||||
|
||||
fields = [
|
||||
f"`{'tab' + child_doc.doctype}`.`{batch_no_field}`",
|
||||
f"`{'tab' + child_doc.doctype}`.`batch_no`",
|
||||
f"`{'tab' + child_doc.doctype}`.`stock_qty`",
|
||||
]
|
||||
|
||||
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],
|
||||
]
|
||||
|
||||
if batch_no_field == "rejected_serial_and_batch_bundle":
|
||||
filters.append([child_doc.doctype, "rejected_qty", ">", 0])
|
||||
|
||||
# Required for POS Invoice
|
||||
if ignore_voucher_detail_no:
|
||||
filters.append([child_doc.doctype, "name", "!=", ignore_voucher_detail_no])
|
||||
|
||||
ids = []
|
||||
for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters):
|
||||
ids.append(row.get("serial_and_batch_bundle"))
|
||||
if row.get(old_field) and not row.get(batch_no_field):
|
||||
batches.setdefault(row.get(old_field), row.get("stock_qty"))
|
||||
|
||||
if ids:
|
||||
batches.update(get_batches_from_bundle(ids))
|
||||
|
||||
return batches
|
||||
|
||||
@@ -12,7 +12,7 @@ from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.stock.doctype.item.item import set_item_default
|
||||
from erpnext.stock.get_item_details import get_bin_details, get_conversion_factor
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
from erpnext.stock.utils import get_incoming_rate, get_valuation_method
|
||||
|
||||
|
||||
class SellingController(StockController):
|
||||
@@ -308,6 +308,8 @@ class SellingController(StockController):
|
||||
"warehouse": p.warehouse or d.warehouse,
|
||||
"item_code": p.item_code,
|
||||
"qty": flt(p.qty),
|
||||
"serial_no": p.serial_no if self.docstatus == 2 else None,
|
||||
"batch_no": p.batch_no if self.docstatus == 2 else None,
|
||||
"uom": p.uom,
|
||||
"serial_and_batch_bundle": p.serial_and_batch_bundle
|
||||
or get_serial_and_batch_bundle(p, self),
|
||||
@@ -330,6 +332,8 @@ class SellingController(StockController):
|
||||
"warehouse": d.warehouse,
|
||||
"item_code": d.item_code,
|
||||
"qty": d.stock_qty,
|
||||
"serial_no": d.serial_no if self.docstatus == 2 else None,
|
||||
"batch_no": d.batch_no if self.docstatus == 2 else None,
|
||||
"uom": d.uom,
|
||||
"stock_uom": d.stock_uom,
|
||||
"conversion_factor": d.conversion_factor,
|
||||
@@ -428,11 +432,13 @@ class SellingController(StockController):
|
||||
|
||||
items = self.get("items") + (self.get("packed_items") or [])
|
||||
for d in items:
|
||||
if not self.get("return_against"):
|
||||
if not self.get("return_against") or (
|
||||
get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")
|
||||
):
|
||||
# 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 (self.get("is_return") and d.incoming_rate):
|
||||
if not d.incoming_rate:
|
||||
d.incoming_rate = get_incoming_rate(
|
||||
{
|
||||
"item_code": d.item_code,
|
||||
|
||||
@@ -455,6 +455,12 @@ class StockController(AccountsController):
|
||||
sl_dict.update(args)
|
||||
self.update_inventory_dimensions(d, sl_dict)
|
||||
|
||||
if self.docstatus == 2:
|
||||
# To handle denormalized serial no records, will br deprecated in v16
|
||||
for field in ["serial_no", "batch_no"]:
|
||||
if d.get(field):
|
||||
sl_dict[field] = d.get(field)
|
||||
|
||||
return sl_dict
|
||||
|
||||
def update_inventory_dimensions(self, row, sl_dict) -> None:
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
"idx": 5,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2023-08-28 22:28:00.104413",
|
||||
"modified": "2023-12-01 18:46:49.468526",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Lead",
|
||||
@@ -577,6 +577,7 @@
|
||||
],
|
||||
"search_fields": "lead_name,lead_owner,status",
|
||||
"sender_field": "email_id",
|
||||
"sender_name_field": "lead_name",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
|
||||
@@ -14,6 +14,7 @@ from frappe.utils import comma_and, get_link_to_form, has_gravatar, validate_ema
|
||||
from erpnext.accounts.party import set_taxes
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_open_events
|
||||
from erpnext.selling.doctype.customer.customer import parse_full_name
|
||||
|
||||
|
||||
class Lead(SellingController, CRMNote):
|
||||
@@ -111,6 +112,10 @@ class Lead(SellingController, CRMNote):
|
||||
return
|
||||
self.contact_doc = self.create_contact()
|
||||
|
||||
# leads created by email inbox only have the full name set
|
||||
if self.lead_name and not any([self.first_name, self.middle_name, self.last_name]):
|
||||
self.first_name, self.middle_name, self.last_name = parse_full_name(self.lead_name)
|
||||
|
||||
def after_insert(self):
|
||||
self.link_to_contact()
|
||||
|
||||
|
||||
@@ -637,6 +637,7 @@ additional_timeline_content = {
|
||||
|
||||
extend_bootinfo = [
|
||||
"erpnext.support.doctype.service_level_agreement.service_level_agreement.add_sla_doctypes",
|
||||
"erpnext.startup.boot.bootinfo",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -583,6 +583,7 @@ class ProductionPlan(Document):
|
||||
|
||||
if close:
|
||||
self.db_set("status", "Closed")
|
||||
self.update_bin_qty()
|
||||
return
|
||||
|
||||
if self.total_produced_qty > 0:
|
||||
@@ -597,6 +598,9 @@ class ProductionPlan(Document):
|
||||
if close is not None:
|
||||
self.db_set("status", self.status)
|
||||
|
||||
if self.docstatus == 1 and self.status != "Completed":
|
||||
self.update_bin_qty()
|
||||
|
||||
def update_ordered_status(self):
|
||||
update_status = False
|
||||
for d in self.po_items:
|
||||
|
||||
@@ -1458,6 +1458,47 @@ class TestProductionPlan(FrappeTestCase):
|
||||
self.assertEqual(row.get("uom"), "Nos")
|
||||
self.assertEqual(row.get("conversion_factor"), 10.0)
|
||||
|
||||
def test_unreserve_qty_on_closing_of_pp(self):
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
from erpnext.stock.utils import get_or_make_bin
|
||||
|
||||
fg_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name
|
||||
rm_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name
|
||||
|
||||
store_warehouse = create_warehouse("Store Warehouse", company="_Test Company")
|
||||
rm_warehouse = create_warehouse("RM Warehouse", company="_Test Company")
|
||||
|
||||
make_bom(item=fg_item, raw_materials=[rm_item], source_warehouse="_Test Warehouse - _TC")
|
||||
|
||||
pln = create_production_plan(
|
||||
item_code=fg_item, planned_qty=10, stock_uom="_Test UOM 1", do_not_submit=1
|
||||
)
|
||||
|
||||
pln.for_warehouse = rm_warehouse
|
||||
mr_items = get_items_for_material_requests(pln.as_dict())
|
||||
for d in mr_items:
|
||||
pln.append("mr_items", d)
|
||||
|
||||
pln.save()
|
||||
pln.submit()
|
||||
|
||||
bin_name = get_or_make_bin(rm_item, rm_warehouse)
|
||||
before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
||||
|
||||
pln.reload()
|
||||
pln.set_status(close=True)
|
||||
|
||||
bin_name = get_or_make_bin(rm_item, rm_warehouse)
|
||||
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
||||
self.assertAlmostEqual(after_qty, before_qty - 10)
|
||||
|
||||
pln.reload()
|
||||
pln.set_status(close=False)
|
||||
|
||||
bin_name = get_or_make_bin(rm_item, rm_warehouse)
|
||||
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
||||
self.assertAlmostEqual(after_qty, before_qty)
|
||||
|
||||
|
||||
def create_production_plan(**args):
|
||||
"""
|
||||
|
||||
@@ -352,3 +352,4 @@ erpnext.patches.v14_0.update_zero_asset_quantity_field
|
||||
execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction")
|
||||
# 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
|
||||
|
||||
@@ -3,6 +3,7 @@ import frappe
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("assets", "doctype", "Asset Depreciation Schedule")
|
||||
frappe.reload_doc("assets", "doctype", "Asset Finance Book")
|
||||
|
||||
assets = get_details_of_draft_or_submitted_depreciable_assets()
|
||||
|
||||
@@ -86,6 +87,7 @@ def get_asset_finance_books_map():
|
||||
afb.frequency_of_depreciation,
|
||||
afb.rate_of_depreciation,
|
||||
afb.expected_value_after_useful_life,
|
||||
afb.daily_prorata_based,
|
||||
afb.shift_based,
|
||||
)
|
||||
.where(asset.docstatus < 2)
|
||||
|
||||
@@ -2,14 +2,6 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Homepage', {
|
||||
setup: function(frm) {
|
||||
frm.fields_dict["products"].grid.get_field("item").get_query = function() {
|
||||
return {
|
||||
filters: {'published': 1}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.add_custom_button(__('Set Meta Tags'), () => {
|
||||
frappe.utils.set_meta_tag('home');
|
||||
|
||||
@@ -36,14 +36,14 @@ erpnext.buying = {
|
||||
|
||||
// no idea where me is coming from
|
||||
if(this.frm.get_field('shipping_address')) {
|
||||
this.frm.set_query("shipping_address", function() {
|
||||
if(me.frm.doc.customer) {
|
||||
this.frm.set_query("shipping_address", () => {
|
||||
if(this.frm.doc.customer) {
|
||||
return {
|
||||
query: 'frappe.contacts.doctype.address.address.address_query',
|
||||
filters: { link_doctype: 'Customer', link_name: me.frm.doc.customer }
|
||||
filters: { link_doctype: 'Customer', link_name: this.frm.doc.customer }
|
||||
};
|
||||
} else
|
||||
return erpnext.queries.company_address_query(me.frm.doc)
|
||||
return erpnext.queries.company_address_query(this.frm.doc)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -361,9 +361,14 @@ erpnext.buying = {
|
||||
new erpnext.SerialBatchPackageSelector(
|
||||
me.frm, item, (r) => {
|
||||
if (r) {
|
||||
let qty = Math.abs(r.total_qty);
|
||||
if (doc.is_return) {
|
||||
qty = qty * -1;
|
||||
}
|
||||
|
||||
let update_values = {
|
||||
"serial_and_batch_bundle": r.name,
|
||||
"qty": Math.abs(r.total_qty)
|
||||
"qty": qty
|
||||
}
|
||||
|
||||
if (r.warehouse) {
|
||||
@@ -396,9 +401,14 @@ erpnext.buying = {
|
||||
new erpnext.SerialBatchPackageSelector(
|
||||
me.frm, item, (r) => {
|
||||
if (r) {
|
||||
let qty = Math.abs(r.total_qty);
|
||||
if (doc.is_return) {
|
||||
qty = qty * -1;
|
||||
}
|
||||
|
||||
let update_values = {
|
||||
"serial_and_batch_bundle": r.name,
|
||||
"rejected_qty": Math.abs(r.total_qty)
|
||||
"rejected_qty": qty
|
||||
}
|
||||
|
||||
if (r.warehouse) {
|
||||
|
||||
@@ -380,6 +380,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
|
||||
scan_barcode() {
|
||||
frappe.flags.dialog_set = false;
|
||||
const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm});
|
||||
barcode_scanner.process_scan();
|
||||
}
|
||||
@@ -512,6 +513,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
cost_center: item.cost_center,
|
||||
tax_category: me.frm.doc.tax_category,
|
||||
item_tax_template: item.item_tax_template,
|
||||
child_doctype: item.doctype,
|
||||
child_docname: item.name,
|
||||
is_old_subcontracting_flow: me.frm.doc.is_old_subcontracting_flow,
|
||||
}
|
||||
|
||||
@@ -2,10 +2,16 @@ frappe.provide("erpnext.financial_statements");
|
||||
|
||||
erpnext.financial_statements = {
|
||||
"filters": get_filters(),
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
"formatter": function(value, row, column, data, default_formatter, filter) {
|
||||
if (data && column.fieldname=="account") {
|
||||
value = data.account_name || value;
|
||||
|
||||
if (filter && filter?.text && filter?.type == "contains") {
|
||||
if (!value.toLowerCase().includes(filter.text)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.account) {
|
||||
column.link_onclick =
|
||||
"erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")";
|
||||
|
||||
@@ -8,7 +8,7 @@ $.extend(erpnext, {
|
||||
if(!company && cur_frm)
|
||||
company = cur_frm.doc.company;
|
||||
if(company)
|
||||
return frappe.get_doc(":Company", company).default_currency || frappe.boot.sysdefaults.currency;
|
||||
return frappe.get_doc(":Company", company)?.default_currency || frappe.boot.sysdefaults.currency;
|
||||
else
|
||||
return frappe.boot.sysdefaults.currency;
|
||||
},
|
||||
@@ -1077,7 +1077,7 @@ function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) {
|
||||
}
|
||||
|
||||
function get_time_left(timestamp, agreement_status) {
|
||||
const diff = moment(timestamp).diff(moment());
|
||||
const diff = moment(timestamp).diff(frappe.datetime.system_datetime(true));
|
||||
const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : 'Failed';
|
||||
let indicator = (diff_display == 'Failed' && agreement_status != 'Fulfilled') ? 'red' : 'green';
|
||||
return {'diff_display': diff_display, 'indicator': indicator};
|
||||
|
||||
@@ -114,13 +114,13 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
|
||||
frappe.run_serially([
|
||||
() => this.set_selector_trigger_flag(data),
|
||||
() => this.set_serial_no(row, serial_no),
|
||||
() => this.set_batch_no(row, batch_no),
|
||||
() => this.set_barcode(row, barcode),
|
||||
() => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => {
|
||||
this.show_scan_message(row.idx, row.item_code, qty);
|
||||
}),
|
||||
() => this.set_barcode_uom(row, uom),
|
||||
() => this.set_serial_no(row, serial_no),
|
||||
() => this.set_batch_no(row, batch_no),
|
||||
() => this.set_barcode(row, barcode),
|
||||
() => this.clean_up(),
|
||||
() => this.revert_selector_flag(),
|
||||
() => resolve(row)
|
||||
@@ -131,10 +131,10 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
// batch and serial selector is reduandant when all info can be added by scan
|
||||
// this flag on item row is used by transaction.js to avoid triggering selector
|
||||
set_selector_trigger_flag(data) {
|
||||
const {batch_no, serial_no, has_batch_no, has_serial_no} = data;
|
||||
const {has_batch_no, has_serial_no} = data;
|
||||
|
||||
const require_selecting_batch = has_batch_no && !batch_no;
|
||||
const require_selecting_serial = has_serial_no && !serial_no;
|
||||
const require_selecting_batch = has_batch_no;
|
||||
const require_selecting_serial = has_serial_no;
|
||||
|
||||
if (!(require_selecting_batch || require_selecting_serial)) {
|
||||
frappe.flags.hide_serial_batch_dialog = true;
|
||||
|
||||
@@ -317,9 +317,14 @@ erpnext.sales_common = {
|
||||
new erpnext.SerialBatchPackageSelector(
|
||||
me.frm, item, (r) => {
|
||||
if (r) {
|
||||
let qty = Math.abs(r.total_qty);
|
||||
if (doc.is_return) {
|
||||
qty = qty * -1;
|
||||
}
|
||||
|
||||
frappe.model.set_value(item.doctype, item.name, {
|
||||
"serial_and_batch_bundle": r.name,
|
||||
"qty": Math.abs(r.total_qty)
|
||||
"qty": qty
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,19 +31,40 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
secondary_action: () => this.edit_full_form(),
|
||||
});
|
||||
|
||||
this.dialog.set_value("qty", this.item.qty).then(() => {
|
||||
if (this.item.serial_no) {
|
||||
this.dialog.set_value("scan_serial_no", this.item.serial_no);
|
||||
frappe.model.set_value(this.item.doctype, this.item.name, 'serial_no', '');
|
||||
} else if (this.item.batch_no) {
|
||||
this.dialog.set_value("scan_batch_no", this.item.batch_no);
|
||||
frappe.model.set_value(this.item.doctype, this.item.name, 'batch_no', '');
|
||||
}
|
||||
});
|
||||
|
||||
this.dialog.show();
|
||||
this.$scan_btn = this.dialog.$wrapper.find(".link-btn");
|
||||
this.$scan_btn.css("display", "inline");
|
||||
|
||||
let qty = this.item.stock_qty || this.item.transfer_qty || this.item.qty;
|
||||
|
||||
if (this.item?.is_rejected) {
|
||||
qty = this.item.rejected_qty;
|
||||
}
|
||||
|
||||
qty = Math.abs(qty);
|
||||
if (qty > 0) {
|
||||
this.dialog.set_value("qty", qty).then(() => {
|
||||
if (this.item.serial_no && !this.item.serial_and_batch_bundle) {
|
||||
let serial_nos = this.item.serial_no.split('\n');
|
||||
if (serial_nos.length > 1) {
|
||||
serial_nos.forEach(serial_no => {
|
||||
this.dialog.fields_dict.entries.df.data.push({
|
||||
serial_no: serial_no,
|
||||
batch_no: this.item.batch_no
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.dialog.set_value("scan_serial_no", this.item.serial_no);
|
||||
}
|
||||
frappe.model.set_value(this.item.doctype, this.item.name, 'serial_no', '');
|
||||
} else if (this.item.batch_no && !this.item.serial_and_batch_bundle) {
|
||||
this.dialog.set_value("scan_batch_no", this.item.batch_no);
|
||||
frappe.model.set_value(this.item.doctype, this.item.name, 'batch_no', '');
|
||||
}
|
||||
|
||||
this.dialog.fields_dict.entries.grid.refresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get_serial_no_filters() {
|
||||
@@ -463,13 +484,13 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
}
|
||||
|
||||
render_data() {
|
||||
if (!this.frm.is_new() && this.bundle) {
|
||||
if (this.bundle) {
|
||||
frappe.call({
|
||||
method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.get_serial_batch_ledgers',
|
||||
args: {
|
||||
item_code: this.item.item_code,
|
||||
name: this.bundle,
|
||||
voucher_no: this.item.parent,
|
||||
voucher_no: !this.frm.is_new() ? this.item.parent : "",
|
||||
}
|
||||
}).then(r => {
|
||||
if (r.message) {
|
||||
|
||||
@@ -112,9 +112,9 @@ def create_transaction(doctype, company, start_date):
|
||||
warehouse = get_warehouse(company)
|
||||
|
||||
if document_type == "Purchase Order":
|
||||
posting_date = get_random_date(start_date, 1, 30)
|
||||
posting_date = get_random_date(start_date, 1, 25)
|
||||
else:
|
||||
posting_date = get_random_date(start_date, 31, 364)
|
||||
posting_date = get_random_date(start_date, 31, 350)
|
||||
|
||||
doctype.update(
|
||||
{
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import os
|
||||
import json
|
||||
import os
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
@@ -114,10 +114,11 @@ def update_regional_tax_settings(country, company):
|
||||
frappe.scrub(country)
|
||||
)
|
||||
frappe.get_attr(module_name)(country, company)
|
||||
except Exception as e:
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
except Exception:
|
||||
# Log error and ignore if failed to setup regional tax settings
|
||||
frappe.log_error("Unable to setup regional tax settings")
|
||||
pass
|
||||
|
||||
|
||||
def make_taxes_and_charges_template(company_name, doctype, template):
|
||||
|
||||
@@ -75,3 +75,11 @@ def update_page_info(bootinfo):
|
||||
"Sales Person Tree": {"title": "Sales Person Tree", "route": "Tree/Sales Person"},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def bootinfo(bootinfo):
|
||||
if bootinfo.get("user") and bootinfo["user"].get("name"):
|
||||
bootinfo["user"]["employee"] = ""
|
||||
employee = frappe.db.get_value("Employee", {"user_id": bootinfo["user"]["name"]}, "name")
|
||||
if employee:
|
||||
bootinfo["user"]["employee"] = employee
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import frappe
|
||||
from frappe.utils import cint
|
||||
from frappe.utils.deprecations import deprecated
|
||||
|
||||
|
||||
def get_leaderboards():
|
||||
@@ -54,12 +54,13 @@ def get_leaderboards():
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_all_customers(date_range, company, field, limit=None):
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company]]
|
||||
from_date, to_date = parse_date_range(date_range)
|
||||
if field == "outstanding_amount":
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company]]
|
||||
if date_range:
|
||||
date_range = frappe.parse_json(date_range)
|
||||
filters.append(["posting_date", ">=", "between", [date_range[0], date_range[1]]])
|
||||
return frappe.db.get_all(
|
||||
if from_date and to_date:
|
||||
filters.append(["posting_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.get_list(
|
||||
"Sales Invoice",
|
||||
fields=["customer as name", "sum(outstanding_amount) as value"],
|
||||
filters=filters,
|
||||
@@ -69,26 +70,20 @@ def get_all_customers(date_range, company, field, limit=None):
|
||||
)
|
||||
else:
|
||||
if field == "total_sales_amount":
|
||||
select_field = "sum(so_item.base_net_amount)"
|
||||
select_field = "base_net_total"
|
||||
elif field == "total_qty_sold":
|
||||
select_field = "sum(so_item.stock_qty)"
|
||||
select_field = "total_qty"
|
||||
|
||||
date_condition = get_date_condition(date_range, "so.transaction_date")
|
||||
if from_date and to_date:
|
||||
filters.append(["transaction_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select so.customer as name, {0} as value
|
||||
FROM `tabSales Order` as so JOIN `tabSales Order Item` as so_item
|
||||
ON so.name = so_item.parent
|
||||
where so.docstatus = 1 {1} and so.company = %s
|
||||
group by so.customer
|
||||
order by value DESC
|
||||
limit %s
|
||||
""".format(
|
||||
select_field, date_condition
|
||||
),
|
||||
(company, cint(limit)),
|
||||
as_dict=1,
|
||||
return frappe.get_list(
|
||||
"Sales Order",
|
||||
fields=["customer as name", f"sum({select_field}) as value"],
|
||||
filters=filters,
|
||||
group_by="customer",
|
||||
order_by="value desc",
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@@ -96,55 +91,58 @@ def get_all_customers(date_range, company, field, limit=None):
|
||||
def get_all_items(date_range, company, field, limit=None):
|
||||
if field in ("available_stock_qty", "available_stock_value"):
|
||||
select_field = "sum(actual_qty)" if field == "available_stock_qty" else "sum(stock_value)"
|
||||
return frappe.db.get_all(
|
||||
results = frappe.db.get_all(
|
||||
"Bin",
|
||||
fields=["item_code as name", "{0} as value".format(select_field)],
|
||||
group_by="item_code",
|
||||
order_by="value desc",
|
||||
limit=limit,
|
||||
)
|
||||
readable_active_items = set(frappe.get_list("Item", filters={"disabled": 0}, pluck="name"))
|
||||
return [item for item in results if item["name"] in readable_active_items]
|
||||
else:
|
||||
if field == "total_sales_amount":
|
||||
select_field = "sum(order_item.base_net_amount)"
|
||||
select_field = "base_net_amount"
|
||||
select_doctype = "Sales Order"
|
||||
elif field == "total_purchase_amount":
|
||||
select_field = "sum(order_item.base_net_amount)"
|
||||
select_field = "base_net_amount"
|
||||
select_doctype = "Purchase Order"
|
||||
elif field == "total_qty_sold":
|
||||
select_field = "sum(order_item.stock_qty)"
|
||||
select_field = "stock_qty"
|
||||
select_doctype = "Sales Order"
|
||||
elif field == "total_qty_purchased":
|
||||
select_field = "sum(order_item.stock_qty)"
|
||||
select_field = "stock_qty"
|
||||
select_doctype = "Purchase Order"
|
||||
|
||||
date_condition = get_date_condition(date_range, "sales_order.transaction_date")
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company]]
|
||||
from_date, to_date = parse_date_range(date_range)
|
||||
if from_date and to_date:
|
||||
filters.append(["transaction_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select order_item.item_code as name, {0} as value
|
||||
from `tab{1}` sales_order join `tab{1} Item` as order_item
|
||||
on sales_order.name = order_item.parent
|
||||
where sales_order.docstatus = 1
|
||||
and sales_order.company = %s {2}
|
||||
group by order_item.item_code
|
||||
order by value desc
|
||||
limit %s
|
||||
""".format(
|
||||
select_field, select_doctype, date_condition
|
||||
),
|
||||
(company, cint(limit)),
|
||||
as_dict=1,
|
||||
) # nosec
|
||||
child_doctype = f"{select_doctype} Item"
|
||||
return frappe.get_list(
|
||||
select_doctype,
|
||||
fields=[
|
||||
f"`tab{child_doctype}`.item_code as name",
|
||||
f"sum(`tab{child_doctype}`.{select_field}) as value",
|
||||
],
|
||||
filters=filters,
|
||||
order_by="value desc",
|
||||
group_by=f"`tab{child_doctype}`.item_code",
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_all_suppliers(date_range, company, field, limit=None):
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company]]
|
||||
from_date, to_date = parse_date_range(date_range)
|
||||
|
||||
if field == "outstanding_amount":
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company]]
|
||||
if date_range:
|
||||
date_range = frappe.parse_json(date_range)
|
||||
filters.append(["posting_date", "between", [date_range[0], date_range[1]]])
|
||||
return frappe.db.get_all(
|
||||
if from_date and to_date:
|
||||
filters.append(["posting_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.get_list(
|
||||
"Purchase Invoice",
|
||||
fields=["supplier as name", "sum(outstanding_amount) as value"],
|
||||
filters=filters,
|
||||
@@ -154,48 +152,40 @@ def get_all_suppliers(date_range, company, field, limit=None):
|
||||
)
|
||||
else:
|
||||
if field == "total_purchase_amount":
|
||||
select_field = "sum(purchase_order_item.base_net_amount)"
|
||||
select_field = "base_net_total"
|
||||
elif field == "total_qty_purchased":
|
||||
select_field = "sum(purchase_order_item.stock_qty)"
|
||||
select_field = "total_qty"
|
||||
|
||||
date_condition = get_date_condition(date_range, "purchase_order.modified")
|
||||
if from_date and to_date:
|
||||
filters.append(["transaction_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select purchase_order.supplier as name, {0} as value
|
||||
FROM `tabPurchase Order` as purchase_order LEFT JOIN `tabPurchase Order Item`
|
||||
as purchase_order_item ON purchase_order.name = purchase_order_item.parent
|
||||
where
|
||||
purchase_order.docstatus = 1
|
||||
{1}
|
||||
and purchase_order.company = %s
|
||||
group by purchase_order.supplier
|
||||
order by value DESC
|
||||
limit %s""".format(
|
||||
select_field, date_condition
|
||||
),
|
||||
(company, cint(limit)),
|
||||
as_dict=1,
|
||||
) # nosec
|
||||
return frappe.get_list(
|
||||
"Purchase Order",
|
||||
fields=["supplier as name", f"sum({select_field}) as value"],
|
||||
filters=filters,
|
||||
group_by="supplier",
|
||||
order_by="value desc",
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_all_sales_partner(date_range, company, field, limit=None):
|
||||
if field == "total_sales_amount":
|
||||
select_field = "sum(`base_net_total`)"
|
||||
select_field = "base_net_total"
|
||||
elif field == "total_commission":
|
||||
select_field = "sum(`total_commission`)"
|
||||
select_field = "total_commission"
|
||||
|
||||
filters = {"sales_partner": ["!=", ""], "docstatus": 1, "company": company}
|
||||
if date_range:
|
||||
date_range = frappe.parse_json(date_range)
|
||||
filters["transaction_date"] = ["between", [date_range[0], date_range[1]]]
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company], ["sales_partner", "is", "set"]]
|
||||
from_date, to_date = parse_date_range(date_range)
|
||||
if from_date and to_date:
|
||||
filters.append(["transaction_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.get_list(
|
||||
"Sales Order",
|
||||
fields=[
|
||||
"`sales_partner` as name",
|
||||
"{} as value".format(select_field),
|
||||
"sales_partner as name",
|
||||
f"sum({select_field}) as value",
|
||||
],
|
||||
filters=filters,
|
||||
group_by="sales_partner",
|
||||
@@ -206,27 +196,29 @@ def get_all_sales_partner(date_range, company, field, limit=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_all_sales_person(date_range, company, field=None, limit=0):
|
||||
date_condition = get_date_condition(date_range, "sales_order.transaction_date")
|
||||
filters = [
|
||||
["docstatus", "=", "1"],
|
||||
["company", "=", company],
|
||||
["Sales Team", "sales_person", "is", "set"],
|
||||
]
|
||||
from_date, to_date = parse_date_range(date_range)
|
||||
if from_date and to_date:
|
||||
filters.append(["transaction_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select sales_team.sales_person as name, sum(sales_order.base_net_total) as value
|
||||
from `tabSales Order` as sales_order join `tabSales Team` as sales_team
|
||||
on sales_order.name = sales_team.parent and sales_team.parenttype = 'Sales Order'
|
||||
where sales_order.docstatus = 1
|
||||
and sales_order.company = %s
|
||||
{date_condition}
|
||||
group by sales_team.sales_person
|
||||
order by value DESC
|
||||
limit %s
|
||||
""".format(
|
||||
date_condition=date_condition
|
||||
),
|
||||
(company, cint(limit)),
|
||||
as_dict=1,
|
||||
return frappe.get_list(
|
||||
"Sales Order",
|
||||
fields=[
|
||||
"`tabSales Team`.sales_person as name",
|
||||
"sum(`tabSales Team`.allocated_amount) as value",
|
||||
],
|
||||
filters=filters,
|
||||
group_by="`tabSales Team`.sales_person",
|
||||
order_by="value desc",
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@deprecated
|
||||
def get_date_condition(date_range, field):
|
||||
date_condition = ""
|
||||
if date_range:
|
||||
@@ -236,3 +228,11 @@ def get_date_condition(date_range, field):
|
||||
field, frappe.db.escape(from_date), frappe.db.escape(to_date)
|
||||
)
|
||||
return date_condition
|
||||
|
||||
|
||||
def parse_date_range(date_range):
|
||||
if date_range:
|
||||
date_range = frappe.parse_json(date_range)
|
||||
return date_range[0], date_range[1]
|
||||
|
||||
return None, None
|
||||
|
||||
@@ -301,7 +301,8 @@
|
||||
"no_copy": 1,
|
||||
"options": "Delivery Note",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@@ -1401,7 +1402,7 @@
|
||||
"idx": 146,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-04 14:15:28.363184",
|
||||
"modified": "2023-12-18 17:19:39.368239",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note",
|
||||
|
||||
@@ -1294,7 +1294,3 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Delivery Note", ["customer", "is_return", "return_against"])
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
"""Drop unused return_against index"""
|
||||
|
||||
try:
|
||||
frappe.db.sql_ddl(
|
||||
"ALTER TABLE `tabDelivery Note` DROP INDEX `customer_is_return_return_against_index`"
|
||||
)
|
||||
frappe.db.sql_ddl(
|
||||
"ALTER TABLE `tabPurchase Receipt` DROP INDEX `supplier_is_return_return_against_index`"
|
||||
)
|
||||
except Exception:
|
||||
frappe.log_error("Failed to drop unused index")
|
||||
@@ -174,6 +174,115 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
for field, value in field_values.items():
|
||||
self.assertEqual(cstr(serial_no.get(field)), value)
|
||||
|
||||
def test_delivery_note_return_against_denormalized_serial_no(self):
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
frappe.flags.ignore_serial_batch_bundle_validation = True
|
||||
sn_item = "Old Serial NO Item Return Test - 1"
|
||||
make_item(
|
||||
sn_item,
|
||||
{
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "OSN-.####",
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
)
|
||||
|
||||
frappe.flags.ignore_serial_batch_bundle_validation = True
|
||||
serial_nos = [
|
||||
"OSN-1",
|
||||
"OSN-2",
|
||||
"OSN-3",
|
||||
"OSN-4",
|
||||
"OSN-5",
|
||||
"OSN-6",
|
||||
"OSN-7",
|
||||
"OSN-8",
|
||||
"OSN-9",
|
||||
"OSN-10",
|
||||
"OSN-11",
|
||||
"OSN-12",
|
||||
]
|
||||
|
||||
for sn in serial_nos:
|
||||
if not frappe.db.exists("Serial No", sn):
|
||||
sn_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Serial No",
|
||||
"item_code": sn_item,
|
||||
"serial_no": sn,
|
||||
}
|
||||
)
|
||||
sn_doc.insert()
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
company = frappe.db.get_value("Warehouse", warehouse, "company")
|
||||
se_doc = make_stock_entry(
|
||||
item_code=sn_item,
|
||||
company=company,
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=12,
|
||||
basic_rate=100,
|
||||
do_not_submit=1,
|
||||
)
|
||||
|
||||
se_doc.items[0].serial_no = "\n".join(serial_nos)
|
||||
se_doc.submit()
|
||||
|
||||
self.assertEqual(sorted(get_serial_nos(se_doc.items[0].serial_no)), sorted(serial_nos))
|
||||
|
||||
dn = create_delivery_note(
|
||||
item_code=sn_item,
|
||||
qty=12,
|
||||
rate=500,
|
||||
warehouse=warehouse,
|
||||
company=company,
|
||||
expense_account="Cost of Goods Sold - _TC",
|
||||
cost_center="Main - _TC",
|
||||
do_not_submit=1,
|
||||
)
|
||||
|
||||
dn.items[0].serial_no = "\n".join(serial_nos)
|
||||
dn.submit()
|
||||
dn.reload()
|
||||
|
||||
self.assertTrue(dn.items[0].serial_no)
|
||||
|
||||
frappe.flags.ignore_serial_batch_bundle_validation = False
|
||||
|
||||
# return entry
|
||||
dn1 = make_sales_return(dn.name)
|
||||
|
||||
dn1.items[0].qty = -2
|
||||
|
||||
bundle_doc = frappe.get_doc("Serial and Batch Bundle", dn1.items[0].serial_and_batch_bundle)
|
||||
bundle_doc.set("entries", bundle_doc.entries[:2])
|
||||
bundle_doc.save()
|
||||
|
||||
dn1.save()
|
||||
dn1.submit()
|
||||
|
||||
returned_serial_nos1 = get_serial_nos_from_bundle(dn1.items[0].serial_and_batch_bundle)
|
||||
for serial_no in returned_serial_nos1:
|
||||
self.assertTrue(serial_no in serial_nos)
|
||||
|
||||
dn2 = make_sales_return(dn.name)
|
||||
|
||||
dn2.items[0].qty = -2
|
||||
|
||||
bundle_doc = frappe.get_doc("Serial and Batch Bundle", dn2.items[0].serial_and_batch_bundle)
|
||||
bundle_doc.set("entries", bundle_doc.entries[:2])
|
||||
bundle_doc.save()
|
||||
|
||||
dn2.save()
|
||||
dn2.submit()
|
||||
|
||||
returned_serial_nos2 = get_serial_nos_from_bundle(dn2.items[0].serial_and_batch_bundle)
|
||||
for serial_no in returned_serial_nos2:
|
||||
self.assertTrue(serial_no in serial_nos)
|
||||
self.assertFalse(serial_no in returned_serial_nos1)
|
||||
|
||||
def test_sales_return_for_non_bundled_items_partial(self):
|
||||
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
|
||||
|
||||
@@ -1266,6 +1375,56 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
dn.reload()
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Delivered")
|
||||
|
||||
def test_sales_return_valuation_for_moving_average(self):
|
||||
item_code = make_item(
|
||||
"_Test Item Sales Return with MA", {"is_stock_item": 1, "valuation_method": "Moving Average"}
|
||||
).name
|
||||
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=5,
|
||||
basic_rate=100.0,
|
||||
posting_date=add_days(nowdate(), -5),
|
||||
)
|
||||
dn = create_delivery_note(
|
||||
item_code=item_code, qty=5, rate=500, posting_date=add_days(nowdate(), -4)
|
||||
)
|
||||
self.assertEqual(dn.items[0].incoming_rate, 100.0)
|
||||
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=5,
|
||||
basic_rate=200.0,
|
||||
posting_date=add_days(nowdate(), -3),
|
||||
)
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=5,
|
||||
basic_rate=300.0,
|
||||
posting_date=add_days(nowdate(), -2),
|
||||
)
|
||||
|
||||
dn1 = create_delivery_note(
|
||||
is_return=1,
|
||||
item_code=item_code,
|
||||
return_against=dn.name,
|
||||
qty=-5,
|
||||
rate=500,
|
||||
company=dn.company,
|
||||
expense_account="Cost of Goods Sold - _TC",
|
||||
cost_center="Main - _TC",
|
||||
do_not_submit=1,
|
||||
posting_date=add_days(nowdate(), -1),
|
||||
)
|
||||
|
||||
# (300 * 5) + (200 * 5) = 2500
|
||||
# 2500 / 10 = 250
|
||||
|
||||
self.assertAlmostEqual(dn1.items[0].incoming_rate, 250.0)
|
||||
|
||||
|
||||
def create_delivery_note(**args):
|
||||
dn = frappe.new_doc("Delivery Note")
|
||||
|
||||
@@ -522,39 +522,25 @@ class TestItem(FrappeTestCase):
|
||||
self.assertEqual(factor, 1.0)
|
||||
|
||||
def test_item_variant_by_manufacturer(self):
|
||||
fields = [{"field_name": "description"}, {"field_name": "variant_based_on"}]
|
||||
set_item_variant_settings(fields)
|
||||
template = make_item(
|
||||
"_Test Item Variant By Manufacturer", {"has_variants": 1, "variant_based_on": "Manufacturer"}
|
||||
).name
|
||||
|
||||
if frappe.db.exists("Item", "_Test Variant Mfg"):
|
||||
frappe.delete_doc("Item", "_Test Variant Mfg")
|
||||
if frappe.db.exists("Item", "_Test Variant Mfg-1"):
|
||||
frappe.delete_doc("Item", "_Test Variant Mfg-1")
|
||||
if frappe.db.exists("Manufacturer", "MSG1"):
|
||||
frappe.delete_doc("Manufacturer", "MSG1")
|
||||
for manufacturer in ["DFSS", "DASA", "ASAAS"]:
|
||||
if not frappe.db.exists("Manufacturer", manufacturer):
|
||||
m_doc = frappe.new_doc("Manufacturer")
|
||||
m_doc.short_name = manufacturer
|
||||
m_doc.insert()
|
||||
|
||||
template = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Item",
|
||||
item_code="_Test Variant Mfg",
|
||||
has_variant=1,
|
||||
item_group="Products",
|
||||
variant_based_on="Manufacturer",
|
||||
)
|
||||
).insert()
|
||||
self.assertFalse(frappe.db.exists("Item Manufacturer", {"manufacturer": "DFSS"}))
|
||||
variant = get_variant(template, manufacturer="DFSS", manufacturer_part_no="DFSS-123")
|
||||
|
||||
manufacturer = frappe.get_doc(dict(doctype="Manufacturer", short_name="MSG1")).insert()
|
||||
item_manufacturer = frappe.db.exists(
|
||||
"Item Manufacturer", {"manufacturer": "DFSS", "item_code": variant.name}
|
||||
)
|
||||
self.assertTrue(item_manufacturer)
|
||||
|
||||
variant = get_variant(template.name, manufacturer=manufacturer.name)
|
||||
self.assertEqual(variant.item_code, "_Test Variant Mfg-1")
|
||||
self.assertEqual(variant.description, "_Test Variant Mfg")
|
||||
self.assertEqual(variant.manufacturer, "MSG1")
|
||||
variant.insert()
|
||||
|
||||
variant = get_variant(template.name, manufacturer=manufacturer.name, manufacturer_part_no="007")
|
||||
self.assertEqual(variant.item_code, "_Test Variant Mfg-2")
|
||||
self.assertEqual(variant.description, "_Test Variant Mfg")
|
||||
self.assertEqual(variant.manufacturer, "MSG1")
|
||||
self.assertEqual(variant.manufacturer_part_no, "007")
|
||||
frappe.delete_doc("Item Manufacturer", item_manufacturer)
|
||||
|
||||
def test_stock_exists_against_template_item(self):
|
||||
stock_item = frappe.get_all("Stock Ledger Entry", fields=["item_code"], limit=1)
|
||||
|
||||
@@ -199,9 +199,8 @@ frappe.ui.form.on('Material Request', {
|
||||
|
||||
get_item_data: function(frm, item, overwrite_warehouse=false) {
|
||||
if (item && !item.item_code) { return; }
|
||||
frm.call({
|
||||
frappe.call({
|
||||
method: "erpnext.stock.get_item_details.get_item_details",
|
||||
child: item,
|
||||
args: {
|
||||
args: {
|
||||
item_code: item.item_code,
|
||||
|
||||
@@ -169,7 +169,9 @@ class MaterialRequest(BuyingController):
|
||||
def on_submit(self):
|
||||
self.update_requested_qty_in_production_plan()
|
||||
self.update_requested_qty()
|
||||
if self.material_request_type == "Purchase":
|
||||
if self.material_request_type == "Purchase" and frappe.db.exists(
|
||||
"Budget", {"applicable_on_material_request": 1, "docstatus": 1}
|
||||
):
|
||||
self.validate_budget()
|
||||
|
||||
def before_save(self):
|
||||
|
||||
@@ -289,7 +289,8 @@
|
||||
"no_copy": 1,
|
||||
"options": "Purchase Receipt",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_addresses",
|
||||
@@ -1251,7 +1252,7 @@
|
||||
"idx": 261,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-28 13:14:15.243474",
|
||||
"modified": "2023-12-18 17:26:41.279663",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt",
|
||||
|
||||
@@ -1357,10 +1357,6 @@ def get_item_account_wise_additional_cost(purchase_document):
|
||||
return item_account_wise_cost
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Purchase Receipt", ["supplier", "is_return", "return_against"])
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def update_regional_gl_entries(gl_list, doc):
|
||||
return
|
||||
|
||||
@@ -121,7 +121,7 @@ frappe.ui.form.on('Serial and Batch Bundle', {
|
||||
frappe.throw(__("Please attach CSV file"));
|
||||
}
|
||||
|
||||
if (frm.doc.has_serial_no && !prompt_data.using_csv_file && !prompt_data.serial_nos) {
|
||||
if (frm.doc.has_serial_no && !prompt_data.csv_file && !prompt_data.serial_nos) {
|
||||
frappe.throw(__("Please enter serial nos"));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2022-09-29 14:56:38.338267",
|
||||
"creation": "2023-08-11 17:22:12.907518",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
@@ -250,7 +250,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-28 12:56:03.072224",
|
||||
"modified": "2023-12-07 17:56:55.528563",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Serial and Batch Bundle",
|
||||
@@ -270,6 +270,118 @@
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Purchase User",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Purchase Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Stock User",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Stock Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Delivery User",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Delivery Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Manufacturing User",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Manufacturing Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
||||
@@ -23,7 +23,11 @@ from frappe.utils import (
|
||||
)
|
||||
from frappe.utils.csvutils import build_csv_response
|
||||
|
||||
from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
|
||||
from erpnext.stock.serial_batch_bundle import (
|
||||
BatchNoValuation,
|
||||
SerialNoValuation,
|
||||
get_batches_from_bundle,
|
||||
)
|
||||
from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle
|
||||
|
||||
|
||||
@@ -123,6 +127,11 @@ class SerialandBatchBundle(Document):
|
||||
)
|
||||
|
||||
def validate_serial_nos_duplicate(self):
|
||||
# Don't inward same serial number multiple times
|
||||
|
||||
if not self.warehouse:
|
||||
return
|
||||
|
||||
if self.voucher_type in ["Stock Reconciliation", "Stock Entry"] and self.docstatus != 1:
|
||||
return
|
||||
|
||||
@@ -146,7 +155,6 @@ class SerialandBatchBundle(Document):
|
||||
kwargs["voucher_no"] = self.voucher_no
|
||||
|
||||
available_serial_nos = get_available_serial_nos(kwargs)
|
||||
|
||||
for data in available_serial_nos:
|
||||
if data.serial_no in serial_nos:
|
||||
self.throw_error_message(
|
||||
@@ -157,7 +165,7 @@ class SerialandBatchBundle(Document):
|
||||
def throw_error_message(self, message, exception=frappe.ValidationError):
|
||||
frappe.throw(_(message), exception, title=_("Error"))
|
||||
|
||||
def set_incoming_rate(self, row=None, save=False):
|
||||
def set_incoming_rate(self, row=None, save=False, allow_negative_stock=False):
|
||||
if self.type_of_transaction not in ["Inward", "Outward"] or self.voucher_type in [
|
||||
"Installation Note",
|
||||
"Job Card",
|
||||
@@ -167,7 +175,9 @@ class SerialandBatchBundle(Document):
|
||||
return
|
||||
|
||||
if self.type_of_transaction == "Outward":
|
||||
self.set_incoming_rate_for_outward_transaction(row, save)
|
||||
self.set_incoming_rate_for_outward_transaction(
|
||||
row, save, allow_negative_stock=allow_negative_stock
|
||||
)
|
||||
else:
|
||||
self.set_incoming_rate_for_inward_transaction(row, save)
|
||||
|
||||
@@ -188,7 +198,9 @@ class SerialandBatchBundle(Document):
|
||||
def get_serial_nos(self):
|
||||
return [d.serial_no for d in self.entries if d.serial_no]
|
||||
|
||||
def set_incoming_rate_for_outward_transaction(self, row=None, save=False):
|
||||
def set_incoming_rate_for_outward_transaction(
|
||||
self, row=None, save=False, allow_negative_stock=False
|
||||
):
|
||||
sle = self.get_sle_for_outward_transaction()
|
||||
|
||||
if self.has_serial_no:
|
||||
@@ -217,7 +229,8 @@ class SerialandBatchBundle(Document):
|
||||
if self.docstatus == 1:
|
||||
available_qty += flt(d.qty)
|
||||
|
||||
self.validate_negative_batch(d.batch_no, available_qty)
|
||||
if not allow_negative_stock:
|
||||
self.validate_negative_batch(d.batch_no, available_qty)
|
||||
|
||||
d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate)
|
||||
|
||||
@@ -322,6 +335,19 @@ class SerialandBatchBundle(Document):
|
||||
):
|
||||
values_to_set["posting_time"] = parent.posting_time
|
||||
|
||||
if parent.doctype in [
|
||||
"Delivery Note",
|
||||
"Purchase Receipt",
|
||||
"Purchase Invoice",
|
||||
"Sales Invoice",
|
||||
] and parent.get("is_return"):
|
||||
return_ref_field = frappe.scrub(parent.doctype) + "_item"
|
||||
if parent.doctype == "Delivery Note":
|
||||
return_ref_field = "dn_detail"
|
||||
|
||||
if row.get(return_ref_field):
|
||||
values_to_set["returned_against"] = row.get(return_ref_field)
|
||||
|
||||
if values_to_set:
|
||||
self.db_set(values_to_set)
|
||||
|
||||
@@ -433,7 +459,7 @@ class SerialandBatchBundle(Document):
|
||||
qty_field = "qty"
|
||||
|
||||
precision = row.precision
|
||||
if self.voucher_type in ["Subcontracting Receipt"]:
|
||||
if row.get("doctype") in ["Subcontracting Receipt Supplied Item"]:
|
||||
qty_field = "consumed_qty"
|
||||
|
||||
if abs(abs(flt(self.total_qty, precision)) - abs(flt(row.get(qty_field), precision))) > 0.01:
|
||||
@@ -504,8 +530,23 @@ class SerialandBatchBundle(Document):
|
||||
batch_nos = []
|
||||
|
||||
serial_batches = {}
|
||||
|
||||
for row in self.entries:
|
||||
if self.has_serial_no and not row.serial_no:
|
||||
frappe.throw(
|
||||
_("At row {0}: Serial No is mandatory for Item {1}").format(
|
||||
bold(row.idx), bold(self.item_code)
|
||||
),
|
||||
title=_("Serial No is mandatory"),
|
||||
)
|
||||
|
||||
if self.has_batch_no and not row.batch_no:
|
||||
frappe.throw(
|
||||
_("At row {0}: Batch No is mandatory for Item {1}").format(
|
||||
bold(row.idx), bold(self.item_code)
|
||||
),
|
||||
title=_("Batch No is mandatory"),
|
||||
)
|
||||
|
||||
if row.serial_no:
|
||||
serial_nos.append(row.serial_no)
|
||||
|
||||
@@ -569,6 +610,67 @@ class SerialandBatchBundle(Document):
|
||||
f"Batch Nos {bold(incorrect_batch_nos)} does not belong to Item {bold(self.item_code)}"
|
||||
)
|
||||
|
||||
def validate_serial_and_batch_no_for_returned(self):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
if not self.returned_against:
|
||||
return
|
||||
|
||||
if self.voucher_type not in [
|
||||
"Purchase Receipt",
|
||||
"Purchase Invoice",
|
||||
"Sales Invoice",
|
||||
"Delivery Note",
|
||||
]:
|
||||
return
|
||||
|
||||
data = self.get_orignal_document_data()
|
||||
if not data:
|
||||
return
|
||||
|
||||
serial_nos, batches = [], []
|
||||
current_serial_nos = [d.serial_no for d in self.entries if d.serial_no]
|
||||
current_batches = [d.batch_no for d in self.entries if d.batch_no]
|
||||
|
||||
for d in data:
|
||||
if self.has_serial_no:
|
||||
if d.serial_and_batch_bundle:
|
||||
serial_nos = get_serial_nos_from_bundle(d.serial_and_batch_bundle)
|
||||
else:
|
||||
serial_nos = get_serial_nos(d.serial_no)
|
||||
|
||||
elif self.has_batch_no:
|
||||
if d.serial_and_batch_bundle:
|
||||
batches = get_batches_from_bundle(d.serial_and_batch_bundle)
|
||||
else:
|
||||
batches = frappe._dict({d.batch_no: d.stock_qty})
|
||||
|
||||
if batches:
|
||||
batches = [d for d in batches if batches[d] > 0]
|
||||
|
||||
if serial_nos:
|
||||
if not set(current_serial_nos).issubset(set(serial_nos)):
|
||||
self.throw_error_message(
|
||||
f"Serial Nos {bold(', '.join(serial_nos))} are not part of the original document."
|
||||
)
|
||||
|
||||
if batches:
|
||||
if not set(current_batches).issubset(set(batches)):
|
||||
self.throw_error_message(
|
||||
f"Batch Nos {bold(', '.join(batches))} are not part of the original document."
|
||||
)
|
||||
|
||||
def get_orignal_document_data(self):
|
||||
fields = ["serial_and_batch_bundle", "stock_qty"]
|
||||
if self.has_serial_no:
|
||||
fields.append("serial_no")
|
||||
|
||||
elif self.has_batch_no:
|
||||
fields.append("batch_no")
|
||||
|
||||
child_doc = self.voucher_type + " Item"
|
||||
return frappe.get_all(child_doc, fields=fields, filters={"name": self.returned_against})
|
||||
|
||||
def validate_duplicate_serial_and_batch_no(self):
|
||||
serial_nos = []
|
||||
batch_nos = []
|
||||
@@ -667,9 +769,29 @@ class SerialandBatchBundle(Document):
|
||||
for batch in batches:
|
||||
frappe.db.set_value("Batch", batch.name, {"reference_name": None, "reference_doctype": None})
|
||||
|
||||
def before_submit(self):
|
||||
self.validate_serial_and_batch_no_for_returned()
|
||||
self.set_purchase_document_no()
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_serial_nos_inventory()
|
||||
|
||||
def set_purchase_document_no(self):
|
||||
if not self.has_serial_no:
|
||||
return
|
||||
|
||||
if self.total_qty > 0:
|
||||
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
|
||||
sn_table = frappe.qb.DocType("Serial No")
|
||||
(
|
||||
frappe.qb.update(sn_table)
|
||||
.set(
|
||||
sn_table.purchase_document_no,
|
||||
self.voucher_no if not sn_table.purchase_document_no else self.voucher_no,
|
||||
)
|
||||
.where(sn_table.name.isin(serial_nos))
|
||||
).run()
|
||||
|
||||
def validate_serial_and_batch_inventory(self):
|
||||
self.check_future_entries_exists()
|
||||
self.validate_batch_inventory()
|
||||
@@ -688,6 +810,7 @@ class SerialandBatchBundle(Document):
|
||||
"item_code": self.item_code,
|
||||
"warehouse": self.warehouse,
|
||||
"batch_no": batches,
|
||||
"consider_negative_batches": True,
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -698,6 +821,9 @@ class SerialandBatchBundle(Document):
|
||||
available_batches = get_available_batches_qty(available_batches)
|
||||
for batch_no in batches:
|
||||
if batch_no not in available_batches or available_batches[batch_no] < 0:
|
||||
if flt(available_batches.get(batch_no)) < 0:
|
||||
self.validate_negative_batch(batch_no, available_batches[batch_no])
|
||||
|
||||
self.throw_error_message(
|
||||
f"Batch {bold(batch_no)} is not available in the selected warehouse {self.warehouse}"
|
||||
)
|
||||
@@ -789,6 +915,9 @@ def parse_csv_file_to_get_serial_batch(reader):
|
||||
if index == 0:
|
||||
has_serial_no = row[0] == "Serial No"
|
||||
has_batch_no = row[0] == "Batch No"
|
||||
if not has_batch_no:
|
||||
has_batch_no = row[1] == "Batch No"
|
||||
|
||||
continue
|
||||
|
||||
if not row[0]:
|
||||
@@ -805,6 +934,13 @@ def parse_csv_file_to_get_serial_batch(reader):
|
||||
}
|
||||
)
|
||||
|
||||
batch_nos.append(
|
||||
{
|
||||
"batch_no": row[1],
|
||||
"qty": row[2],
|
||||
}
|
||||
)
|
||||
|
||||
serial_nos.append(_dict)
|
||||
elif has_batch_no:
|
||||
batch_nos.append(
|
||||
@@ -840,6 +976,9 @@ def make_serial_nos(item_code, serial_nos):
|
||||
serial_nos_details = []
|
||||
user = frappe.session.user
|
||||
for serial_no in serial_nos:
|
||||
if frappe.db.exists("Serial No", serial_no):
|
||||
continue
|
||||
|
||||
serial_nos_details.append(
|
||||
(
|
||||
serial_no,
|
||||
@@ -870,7 +1009,7 @@ def make_serial_nos(item_code, serial_nos):
|
||||
|
||||
frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
|
||||
|
||||
frappe.msgprint(_("Serial Nos are created successfully"))
|
||||
frappe.msgprint(_("Serial Nos are created successfully"), alert=True)
|
||||
|
||||
|
||||
def make_batch_nos(item_code, batch_nos):
|
||||
@@ -881,6 +1020,9 @@ def make_batch_nos(item_code, batch_nos):
|
||||
batch_nos_details = []
|
||||
user = frappe.session.user
|
||||
for batch_no in batch_nos:
|
||||
if frappe.db.exists("Batch", batch_no):
|
||||
continue
|
||||
|
||||
batch_nos_details.append(
|
||||
(batch_no, batch_no, now(), now(), user, user, item.item_code, item.item_name, item.description)
|
||||
)
|
||||
@@ -899,7 +1041,7 @@ def make_batch_nos(item_code, batch_nos):
|
||||
|
||||
frappe.db.bulk_insert("Batch", fields=fields, values=set(batch_nos_details))
|
||||
|
||||
frappe.msgprint(_("Batch Nos are created successfully"))
|
||||
frappe.msgprint(_("Batch Nos are created successfully"), alert=True)
|
||||
|
||||
|
||||
def parse_serial_nos(data):
|
||||
@@ -1454,7 +1596,8 @@ def get_auto_batch_nos(kwargs):
|
||||
available_batches, stock_ledgers_batches, pos_invoice_batches, sre_reserved_batches
|
||||
)
|
||||
|
||||
available_batches = list(filter(lambda x: x.qty > 0, available_batches))
|
||||
if not kwargs.consider_negative_batches:
|
||||
available_batches = list(filter(lambda x: x.qty > 0, available_batches))
|
||||
|
||||
if not qty:
|
||||
return available_batches
|
||||
|
||||
@@ -368,6 +368,58 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
||||
# Batch does not belong to serial no
|
||||
self.assertRaises(frappe.exceptions.ValidationError, doc.save)
|
||||
|
||||
def test_auto_delete_draft_serial_and_batch_bundle(self):
|
||||
serial_and_batch_code = "New Serial No Auto Delete 1"
|
||||
make_item(
|
||||
serial_and_batch_code,
|
||||
{
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "TEST-SER-VALL-.#####",
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
)
|
||||
|
||||
ste = make_stock_entry(
|
||||
item_code=serial_and_batch_code,
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=1,
|
||||
rate=500,
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
serial_no = "SN-TEST-AUTO-DEL"
|
||||
if not frappe.db.exists("Serial No", serial_no):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Serial No",
|
||||
"serial_no": serial_no,
|
||||
"item_code": serial_and_batch_code,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
bundle_doc = make_serial_batch_bundle(
|
||||
{
|
||||
"item_code": serial_and_batch_code,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"voucher_type": "Stock Entry",
|
||||
"posting_date": ste.posting_date,
|
||||
"posting_time": ste.posting_time,
|
||||
"qty": 1,
|
||||
"serial_nos": [serial_no],
|
||||
"type_of_transaction": "Inward",
|
||||
"do_not_submit": True,
|
||||
}
|
||||
)
|
||||
|
||||
bundle_doc.reload()
|
||||
ste.items[0].serial_and_batch_bundle = bundle_doc.name
|
||||
ste.save()
|
||||
ste.reload()
|
||||
|
||||
ste.delete()
|
||||
self.assertFalse(frappe.db.exists("Serial and Batch Bundle", bundle_doc.name))
|
||||
|
||||
|
||||
def get_batch_from_bundle(bundle):
|
||||
from erpnext.stock.serial_batch_bundle import get_batch_nos
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Serial No",
|
||||
"mandatory_depends_on": "eval:parent.has_serial_no == 1",
|
||||
"options": "Serial No",
|
||||
"search_index": 1
|
||||
},
|
||||
@@ -38,7 +37,6 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Batch No",
|
||||
"mandatory_depends_on": "eval:parent.has_batch_no == 1",
|
||||
"options": "Batch",
|
||||
"search_index": 1
|
||||
},
|
||||
@@ -122,7 +120,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-03 15:29:50.199075",
|
||||
"modified": "2023-12-10 19:47:48.227772",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Serial and Batch Entry",
|
||||
|
||||
@@ -27,8 +27,6 @@
|
||||
"column_break_24",
|
||||
"location",
|
||||
"employee",
|
||||
"delivery_details",
|
||||
"delivery_document_type",
|
||||
"warranty_amc_details",
|
||||
"column_break6",
|
||||
"warranty_expiry_date",
|
||||
@@ -39,7 +37,8 @@
|
||||
"more_info",
|
||||
"company",
|
||||
"column_break_2cmm",
|
||||
"work_order"
|
||||
"work_order",
|
||||
"purchase_document_no"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -153,20 +152,6 @@
|
||||
"options": "Employee",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "delivery_details",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Delivery Details",
|
||||
"oldfieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "delivery_document_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Delivery Document Type",
|
||||
"no_copy": 1,
|
||||
"options": "DocType",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "warranty_amc_details",
|
||||
"fieldtype": "Section Break",
|
||||
@@ -275,12 +260,19 @@
|
||||
{
|
||||
"fieldname": "column_break_2cmm",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_document_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Creation Document No",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-barcode",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-28 15:37:59.489945",
|
||||
"modified": "2023-12-17 10:52:55.767839",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Serial No",
|
||||
|
||||
@@ -41,7 +41,6 @@ class SerialNo(StockController):
|
||||
batch_no: DF.Link | None
|
||||
brand: DF.Link | None
|
||||
company: DF.Link
|
||||
delivery_document_type: DF.Link | None
|
||||
description: DF.Text | None
|
||||
employee: DF.Link | None
|
||||
item_code: DF.Link
|
||||
@@ -51,6 +50,7 @@ class SerialNo(StockController):
|
||||
maintenance_status: DF.Literal[
|
||||
"", "Under Warranty", "Out of Warranty", "Under AMC", "Out of AMC"
|
||||
]
|
||||
purchase_document_no: DF.Data | None
|
||||
purchase_rate: DF.Float
|
||||
serial_no: DF.Data
|
||||
status: DF.Literal["", "Active", "Inactive", "Delivered", "Expired"]
|
||||
@@ -231,26 +231,6 @@ def auto_fetch_serial_number(
|
||||
return sorted([d.get("name") for d in serial_numbers])
|
||||
|
||||
|
||||
def get_delivered_serial_nos(serial_nos):
|
||||
"""
|
||||
Returns serial numbers that delivered from the list of serial numbers
|
||||
"""
|
||||
from frappe.query_builder.functions import Coalesce
|
||||
|
||||
SerialNo = frappe.qb.DocType("Serial No")
|
||||
serial_nos = get_serial_nos(serial_nos)
|
||||
query = (
|
||||
frappe.qb.select(SerialNo.name)
|
||||
.from_(SerialNo)
|
||||
.where((SerialNo.name.isin(serial_nos)) & (Coalesce(SerialNo.delivery_document_type, "") != ""))
|
||||
)
|
||||
|
||||
result = query.run()
|
||||
if result and len(result) > 0:
|
||||
delivered_serial_nos = [row[0] for row in result]
|
||||
return delivered_serial_nos
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_pos_reserved_serial_nos(filters):
|
||||
if isinstance(filters, str):
|
||||
|
||||
@@ -781,10 +781,9 @@ frappe.ui.form.on('Stock Entry Detail', {
|
||||
});
|
||||
refresh_field("items");
|
||||
|
||||
let no_batch_serial_number_value = !d.serial_no;
|
||||
if (d.has_batch_no && !d.has_serial_no) {
|
||||
// check only batch_no for batched item
|
||||
no_batch_serial_number_value = !d.batch_no;
|
||||
let no_batch_serial_number_value = false;
|
||||
if (d.has_serial_no || d.has_batch_no) {
|
||||
no_batch_serial_number_value = true;
|
||||
}
|
||||
|
||||
if (no_batch_serial_number_value && !frappe.flags.hide_serial_batch_dialog && !frappe.flags.dialog_set) {
|
||||
@@ -941,6 +940,7 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
|
||||
}
|
||||
|
||||
scan_barcode() {
|
||||
frappe.flags.dialog_set = false;
|
||||
const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm});
|
||||
barcode_scanner.process_scan();
|
||||
}
|
||||
|
||||
@@ -1737,6 +1737,45 @@ class TestStockEntry(FrappeTestCase):
|
||||
self.assertFalse(doc.is_enqueue_action())
|
||||
frappe.flags.in_test = True
|
||||
|
||||
def test_negative_batch(self):
|
||||
item_code = "Test Negative Batch Item - 001"
|
||||
make_item(
|
||||
item_code,
|
||||
{"has_batch_no": 1, "create_new_batch": 1, "batch_naming_series": "Test-BCH-NNS.#####"},
|
||||
)
|
||||
|
||||
se1 = make_stock_entry(
|
||||
item_code=item_code,
|
||||
purpose="Material Receipt",
|
||||
qty=100,
|
||||
target="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
se1.reload()
|
||||
|
||||
batch_no = get_batch_from_bundle(se1.items[0].serial_and_batch_bundle)
|
||||
|
||||
se2 = make_stock_entry(
|
||||
item_code=item_code,
|
||||
purpose="Material Issue",
|
||||
batch_no=batch_no,
|
||||
qty=10,
|
||||
source="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
se2.reload()
|
||||
|
||||
se3 = make_stock_entry(
|
||||
item_code=item_code,
|
||||
purpose="Material Receipt",
|
||||
qty=100,
|
||||
target="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
se3.reload()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, se1.cancel)
|
||||
|
||||
|
||||
def make_serialized_item(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -181,6 +181,9 @@ class StockLedgerEntry(Document):
|
||||
frappe.throw(_("Actual Qty is mandatory"))
|
||||
|
||||
def validate_serial_batch_no_bundle(self):
|
||||
if self.is_cancelled == 1:
|
||||
return
|
||||
|
||||
item_detail = frappe.get_cached_value(
|
||||
"Item",
|
||||
self.item_code,
|
||||
|
||||
@@ -209,7 +209,7 @@ frappe.ui.form.on("Stock Reconciliation", {
|
||||
|
||||
set_amount_quantity: function(doc, cdt, cdn) {
|
||||
var d = frappe.model.get_doc(cdt, cdn);
|
||||
if (d.qty & d.valuation_rate) {
|
||||
if (d.qty && d.valuation_rate) {
|
||||
frappe.model.set_value(cdt, cdn, "amount", flt(d.qty) * flt(d.valuation_rate));
|
||||
frappe.model.set_value(cdt, cdn, "quantity_difference", flt(d.qty) - flt(d.current_qty));
|
||||
frappe.model.set_value(cdt, cdn, "amount_difference", flt(d.amount) - flt(d.current_amount));
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import Optional
|
||||
import frappe
|
||||
from frappe import _, bold, msgprint
|
||||
from frappe.query_builder.functions import CombineDatetime, Sum
|
||||
from frappe.utils import cint, cstr, flt
|
||||
from frappe.utils import add_to_date, cint, cstr, flt
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.utils import get_company_default
|
||||
@@ -116,9 +116,12 @@ class StockReconciliation(StockController):
|
||||
self.repost_future_sle_and_gle()
|
||||
self.delete_auto_created_batches()
|
||||
|
||||
def set_current_serial_and_batch_bundle(self):
|
||||
def set_current_serial_and_batch_bundle(self, voucher_detail_no=None, save=False) -> None:
|
||||
"""Set Serial and Batch Bundle for each item"""
|
||||
for item in self.items:
|
||||
if voucher_detail_no and voucher_detail_no != item.name:
|
||||
continue
|
||||
|
||||
item_details = frappe.get_cached_value(
|
||||
"Item", item.item_code, ["has_serial_no", "has_batch_no"], as_dict=1
|
||||
)
|
||||
@@ -176,6 +179,7 @@ class StockReconciliation(StockController):
|
||||
"warehouse": item.warehouse,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"ignore_voucher_nos": [self.name],
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -191,11 +195,36 @@ class StockReconciliation(StockController):
|
||||
)
|
||||
|
||||
if not serial_and_batch_bundle.entries:
|
||||
if voucher_detail_no:
|
||||
return
|
||||
|
||||
continue
|
||||
|
||||
item.current_serial_and_batch_bundle = serial_and_batch_bundle.save().name
|
||||
serial_and_batch_bundle.save()
|
||||
item.current_serial_and_batch_bundle = serial_and_batch_bundle.name
|
||||
item.current_qty = abs(serial_and_batch_bundle.total_qty)
|
||||
item.current_valuation_rate = abs(serial_and_batch_bundle.avg_rate)
|
||||
if save:
|
||||
sle_creation = frappe.db.get_value(
|
||||
"Serial and Batch Bundle", item.serial_and_batch_bundle, "creation"
|
||||
)
|
||||
creation = add_to_date(sle_creation, seconds=-1)
|
||||
item.db_set(
|
||||
{
|
||||
"current_serial_and_batch_bundle": item.current_serial_and_batch_bundle,
|
||||
"current_qty": item.current_qty,
|
||||
"current_valuation_rate": item.current_valuation_rate,
|
||||
"creation": creation,
|
||||
}
|
||||
)
|
||||
|
||||
serial_and_batch_bundle.db_set(
|
||||
{
|
||||
"creation": creation,
|
||||
"voucher_no": self.name,
|
||||
"voucher_detail_no": voucher_detail_no,
|
||||
}
|
||||
)
|
||||
|
||||
def set_new_serial_and_batch_bundle(self):
|
||||
for item in self.items:
|
||||
@@ -737,56 +766,84 @@ class StockReconciliation(StockController):
|
||||
else:
|
||||
self._cancel()
|
||||
|
||||
def recalculate_current_qty(self, item_code, batch_no):
|
||||
def recalculate_current_qty(self, voucher_detail_no, sle_creation, add_new_sle=False):
|
||||
from erpnext.stock.stock_ledger import get_valuation_rate
|
||||
|
||||
sl_entries = []
|
||||
|
||||
for row in self.items:
|
||||
if (
|
||||
not (row.item_code == item_code and row.batch_no == batch_no)
|
||||
and not row.serial_and_batch_bundle
|
||||
):
|
||||
if voucher_detail_no != row.name:
|
||||
continue
|
||||
|
||||
current_qty = 0.0
|
||||
if row.current_serial_and_batch_bundle:
|
||||
self.recalculate_qty_for_serial_and_batch_bundle(row)
|
||||
continue
|
||||
|
||||
current_qty = get_batch_qty_for_stock_reco(
|
||||
item_code, row.warehouse, batch_no, self.posting_date, self.posting_time, self.name
|
||||
)
|
||||
current_qty = self.get_qty_for_serial_and_batch_bundle(row)
|
||||
elif row.batch_no:
|
||||
current_qty = get_batch_qty_for_stock_reco(
|
||||
row.item_code, row.warehouse, row.batch_no, self.posting_date, self.posting_time, self.name
|
||||
)
|
||||
|
||||
precesion = row.precision("current_qty")
|
||||
if flt(current_qty, precesion) == flt(row.current_qty, precesion):
|
||||
continue
|
||||
if flt(current_qty, precesion) != flt(row.current_qty, precesion):
|
||||
val_rate = get_valuation_rate(
|
||||
row.item_code,
|
||||
row.warehouse,
|
||||
self.doctype,
|
||||
self.name,
|
||||
company=self.company,
|
||||
batch_no=row.batch_no,
|
||||
serial_and_batch_bundle=row.current_serial_and_batch_bundle,
|
||||
)
|
||||
|
||||
val_rate = get_valuation_rate(
|
||||
item_code, row.warehouse, self.doctype, self.name, company=self.company, batch_no=batch_no
|
||||
)
|
||||
row.current_valuation_rate = val_rate
|
||||
row.current_qty = current_qty
|
||||
row.db_set(
|
||||
{
|
||||
"current_qty": row.current_qty,
|
||||
"current_valuation_rate": row.current_valuation_rate,
|
||||
"current_amount": flt(row.current_qty * row.current_valuation_rate),
|
||||
}
|
||||
)
|
||||
|
||||
row.current_valuation_rate = val_rate
|
||||
if not row.current_qty and current_qty:
|
||||
sle = self.get_sle_for_items(row)
|
||||
sle.actual_qty = current_qty * -1
|
||||
sle.valuation_rate = val_rate
|
||||
sl_entries.append(sle)
|
||||
if (
|
||||
add_new_sle
|
||||
and not frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0},
|
||||
"name",
|
||||
)
|
||||
and (not row.current_serial_and_batch_bundle and not row.batch_no)
|
||||
):
|
||||
self.set_current_serial_and_batch_bundle(voucher_detail_no, save=True)
|
||||
row.reload()
|
||||
|
||||
row.current_qty = current_qty
|
||||
row.db_set(
|
||||
{
|
||||
"current_qty": row.current_qty,
|
||||
"current_valuation_rate": row.current_valuation_rate,
|
||||
"current_amount": flt(row.current_qty * row.current_valuation_rate),
|
||||
}
|
||||
)
|
||||
if row.current_qty > 0 and row.current_serial_and_batch_bundle:
|
||||
new_sle = self.get_sle_for_items(row)
|
||||
new_sle.actual_qty = row.current_qty * -1
|
||||
new_sle.valuation_rate = row.current_valuation_rate
|
||||
new_sle.creation_time = add_to_date(sle_creation, seconds=-1)
|
||||
new_sle.serial_and_batch_bundle = row.current_serial_and_batch_bundle
|
||||
new_sle.qty_after_transaction = 0.0
|
||||
sl_entries.append(new_sle)
|
||||
|
||||
if sl_entries:
|
||||
self.make_sl_entries(sl_entries, allow_negative_stock=True)
|
||||
self.make_sl_entries(sl_entries, allow_negative_stock=self.has_negative_stock_allowed())
|
||||
if not frappe.db.exists("Repost Item Valuation", {"voucher_no": self.name, "status": "Queued"}):
|
||||
self.repost_future_sle_and_gle(force=True)
|
||||
|
||||
def recalculate_qty_for_serial_and_batch_bundle(self, row):
|
||||
def has_negative_stock_allowed(self):
|
||||
allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
|
||||
|
||||
if all(d.serial_and_batch_bundle and flt(d.qty) == flt(d.current_qty) for d in self.items):
|
||||
allow_negative_stock = True
|
||||
|
||||
return allow_negative_stock
|
||||
|
||||
def get_qty_for_serial_and_batch_bundle(self, row):
|
||||
doc = frappe.get_doc("Serial and Batch Bundle", row.current_serial_and_batch_bundle)
|
||||
precision = doc.entries[0].precision("qty")
|
||||
|
||||
current_qty = 0
|
||||
for d in doc.entries:
|
||||
qty = (
|
||||
get_batch_qty(
|
||||
@@ -799,10 +856,12 @@ class StockReconciliation(StockController):
|
||||
or 0
|
||||
) * -1
|
||||
|
||||
if flt(d.qty, precision) == flt(qty, precision):
|
||||
continue
|
||||
if flt(d.qty, precision) != flt(qty, precision):
|
||||
d.db_set("qty", qty)
|
||||
|
||||
d.db_set("qty", qty)
|
||||
current_qty += qty
|
||||
|
||||
return abs(current_qty)
|
||||
|
||||
|
||||
def get_batch_qty_for_stock_reco(
|
||||
|
||||
@@ -742,13 +742,6 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
||||
|
||||
se2.cancel()
|
||||
|
||||
self.assertTrue(frappe.db.exists("Repost Item Valuation", {"voucher_no": stock_reco.name}))
|
||||
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Repost Item Valuation", {"voucher_no": stock_reco.name}, "status"),
|
||||
"Completed",
|
||||
)
|
||||
|
||||
sle = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"item_code": item_code, "warehouse": warehouse, "is_cancelled": 0},
|
||||
@@ -766,6 +759,68 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
||||
|
||||
self.assertEqual(flt(sle[0].actual_qty), flt(-100.0))
|
||||
|
||||
def test_backdated_stock_reco_entry_with_batch(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
item_code = self.make_item(
|
||||
"Test New Batch Item ABCVSD",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"batch_number_series": "BNS9.####",
|
||||
"create_new_batch": 1,
|
||||
},
|
||||
).name
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
# Stock Reco for 100, Balace Qty 100
|
||||
stock_reco = create_stock_reconciliation(
|
||||
item_code=item_code,
|
||||
posting_date=nowdate(),
|
||||
posting_time="11:00:00",
|
||||
warehouse=warehouse,
|
||||
qty=100,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
sles = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=["actual_qty"],
|
||||
filters={"voucher_no": stock_reco.name, "is_cancelled": 0},
|
||||
)
|
||||
|
||||
self.assertEqual(len(sles), 1)
|
||||
|
||||
stock_reco.reload()
|
||||
batch_no = get_batch_from_bundle(stock_reco.items[0].serial_and_batch_bundle)
|
||||
|
||||
# Stock Reco for 100, Balace Qty 100
|
||||
stock_reco1 = create_stock_reconciliation(
|
||||
item_code=item_code,
|
||||
posting_date=add_days(nowdate(), -1),
|
||||
posting_time="11:00:00",
|
||||
batch_no=batch_no,
|
||||
warehouse=warehouse,
|
||||
qty=60,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
sles = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=["actual_qty"],
|
||||
filters={"voucher_no": stock_reco.name, "is_cancelled": 0},
|
||||
)
|
||||
|
||||
stock_reco1.reload()
|
||||
new_batch_no = get_batch_from_bundle(stock_reco1.items[0].serial_and_batch_bundle)
|
||||
|
||||
self.assertEqual(len(sles), 2)
|
||||
|
||||
for row in sles:
|
||||
if row.actual_qty < 0:
|
||||
self.assertEqual(row.actual_qty, -60)
|
||||
|
||||
def test_update_stock_reconciliation_while_reposting(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
|
||||
@@ -205,6 +205,7 @@
|
||||
"fieldname": "current_serial_and_batch_bundle",
|
||||
"fieldtype": "Link",
|
||||
"label": "Current Serial / Batch Bundle",
|
||||
"no_copy": 1,
|
||||
"options": "Serial and Batch Bundle",
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -216,7 +217,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-26 12:54:34.011915",
|
||||
"modified": "2023-11-02 15:47:07.929550",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Reconciliation Item",
|
||||
|
||||
@@ -8,6 +8,7 @@ import frappe
|
||||
from frappe import _, throw
|
||||
from frappe.model import child_table_fields, default_fields
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.model.utils import get_fetch_values
|
||||
from frappe.query_builder.functions import IfNull, Sum
|
||||
from frappe.utils import add_days, add_months, cint, cstr, flt, getdate
|
||||
|
||||
@@ -357,7 +358,6 @@ def get_basic_details(args, item, overwrite_warehouse=True):
|
||||
"net_amount": 0.0,
|
||||
"discount_percentage": 0.0,
|
||||
"discount_amount": flt(args.discount_amount) or 0.0,
|
||||
"supplier": get_default_supplier(args, item_defaults, item_group_defaults, brand_defaults),
|
||||
"update_stock": args.get("update_stock")
|
||||
if args.get("doctype") in ["Sales Invoice", "Purchase Invoice"]
|
||||
else 0,
|
||||
@@ -377,6 +377,10 @@ def get_basic_details(args, item, overwrite_warehouse=True):
|
||||
}
|
||||
)
|
||||
|
||||
default_supplier = get_default_supplier(args, item_defaults, item_group_defaults, brand_defaults)
|
||||
if default_supplier:
|
||||
out.supplier = default_supplier
|
||||
|
||||
if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"):
|
||||
out.update(calculate_service_end_date(args, item))
|
||||
|
||||
@@ -571,6 +575,9 @@ def get_item_tax_template(args, item, out):
|
||||
item_tax_template = _get_item_tax_template(args, item_group_doc.taxes, out)
|
||||
item_group = item_group_doc.parent_item_group
|
||||
|
||||
if args.get("child_doctype") and item_tax_template:
|
||||
out.update(get_fetch_values(args.get("child_doctype"), "item_tax_template", item_tax_template))
|
||||
|
||||
|
||||
def _get_item_tax_template(args, taxes, out=None, for_validate=False):
|
||||
if out is None:
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import copy
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos as get_serial_nos_from_sle
|
||||
from erpnext.stock.stock_ledger import get_stock_ledger_entries
|
||||
|
||||
|
||||
@@ -15,8 +18,8 @@ def execute(filters=None):
|
||||
|
||||
def get_columns(filters):
|
||||
columns = [
|
||||
{"label": _("Posting Date"), "fieldtype": "Date", "fieldname": "posting_date"},
|
||||
{"label": _("Posting Time"), "fieldtype": "Time", "fieldname": "posting_time"},
|
||||
{"label": _("Posting Date"), "fieldtype": "Date", "fieldname": "posting_date", "width": 120},
|
||||
{"label": _("Posting Time"), "fieldtype": "Time", "fieldname": "posting_time", "width": 90},
|
||||
{
|
||||
"label": _("Voucher Type"),
|
||||
"fieldtype": "Link",
|
||||
@@ -29,7 +32,7 @@ def get_columns(filters):
|
||||
"fieldtype": "Dynamic Link",
|
||||
"fieldname": "voucher_no",
|
||||
"options": "voucher_type",
|
||||
"width": 180,
|
||||
"width": 230,
|
||||
},
|
||||
{
|
||||
"label": _("Company"),
|
||||
@@ -49,7 +52,7 @@ def get_columns(filters):
|
||||
"label": _("Status"),
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "status",
|
||||
"width": 120,
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
"label": _("Serial No"),
|
||||
@@ -62,7 +65,7 @@ def get_columns(filters):
|
||||
"label": _("Valuation Rate"),
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "valuation_rate",
|
||||
"width": 150,
|
||||
"width": 130,
|
||||
},
|
||||
{
|
||||
"label": _("Qty"),
|
||||
@@ -102,15 +105,29 @@ def get_data(filters):
|
||||
}
|
||||
)
|
||||
|
||||
serial_nos = [{"serial_no": row.serial_no, "valuation_rate": row.valuation_rate}]
|
||||
serial_nos = []
|
||||
if row.serial_no:
|
||||
parsed_serial_nos = get_serial_nos_from_sle(row.serial_no)
|
||||
for serial_no in parsed_serial_nos:
|
||||
if filters.get("serial_no") and filters.get("serial_no") != serial_no:
|
||||
continue
|
||||
|
||||
serial_nos.append(
|
||||
{
|
||||
"serial_no": serial_no,
|
||||
"valuation_rate": abs(row.stock_value_difference / row.actual_qty),
|
||||
}
|
||||
)
|
||||
|
||||
if row.serial_and_batch_bundle:
|
||||
serial_nos = bundle_wise_serial_nos.get(row.serial_and_batch_bundle, [])
|
||||
serial_nos.extend(bundle_wise_serial_nos.get(row.serial_and_batch_bundle, []))
|
||||
|
||||
for index, bundle_data in enumerate(serial_nos):
|
||||
if index == 0:
|
||||
args.serial_no = bundle_data.get("serial_no")
|
||||
args.valuation_rate = bundle_data.get("valuation_rate")
|
||||
data.append(args)
|
||||
new_args = copy.deepcopy(args)
|
||||
new_args.serial_no = bundle_data.get("serial_no")
|
||||
new_args.valuation_rate = bundle_data.get("valuation_rate")
|
||||
data.append(new_args)
|
||||
else:
|
||||
data.append(
|
||||
{
|
||||
|
||||
@@ -413,7 +413,7 @@ class StockBalanceReport(object):
|
||||
"fieldname": "bal_val",
|
||||
"fieldtype": "Currency",
|
||||
"width": 100,
|
||||
"options": "currency",
|
||||
"options": "Company:company:default_currency",
|
||||
},
|
||||
{
|
||||
"label": _("Opening Qty"),
|
||||
@@ -427,7 +427,7 @@ class StockBalanceReport(object):
|
||||
"fieldname": "opening_val",
|
||||
"fieldtype": "Currency",
|
||||
"width": 110,
|
||||
"options": "currency",
|
||||
"options": "Company:company:default_currency",
|
||||
},
|
||||
{
|
||||
"label": _("In Qty"),
|
||||
|
||||
@@ -249,6 +249,13 @@ def get_columns(filters):
|
||||
"options": "Serial No",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Serial and Batch Bundle"),
|
||||
"fieldname": "serial_and_batch_bundle",
|
||||
"fieldtype": "Link",
|
||||
"options": "Serial and Batch Bundle",
|
||||
"width": 100,
|
||||
},
|
||||
{"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 100},
|
||||
{
|
||||
"label": _("Project"),
|
||||
@@ -287,6 +294,7 @@ def get_stock_ledger_entries(filters, items):
|
||||
sle.voucher_type,
|
||||
sle.qty_after_transaction,
|
||||
sle.stock_value_difference,
|
||||
sle.serial_and_batch_bundle,
|
||||
sle.voucher_no,
|
||||
sle.stock_value,
|
||||
sle.batch_no,
|
||||
|
||||
@@ -24,6 +24,7 @@ SLE_FIELDS = (
|
||||
"stock_value_difference",
|
||||
"valuation_rate",
|
||||
"voucher_detail_no",
|
||||
"serial_and_batch_bundle",
|
||||
)
|
||||
|
||||
|
||||
@@ -64,7 +65,11 @@ def add_invariant_check_fields(sles):
|
||||
|
||||
balance_qty += sle.actual_qty
|
||||
balance_stock_value += sle.stock_value_difference
|
||||
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
|
||||
if (
|
||||
sle.voucher_type == "Stock Reconciliation"
|
||||
and not sle.batch_no
|
||||
and not sle.serial_and_batch_bundle
|
||||
):
|
||||
balance_qty = frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "qty")
|
||||
if balance_qty is None:
|
||||
balance_qty = sle.qty_after_transaction
|
||||
@@ -143,6 +148,12 @@ def get_columns():
|
||||
"label": _("Batch"),
|
||||
"options": "Batch",
|
||||
},
|
||||
{
|
||||
"fieldname": "serial_and_batch_bundle",
|
||||
"fieldtype": "Link",
|
||||
"label": _("Serial and Batch Bundle"),
|
||||
"options": "Serial and Batch Bundle",
|
||||
},
|
||||
{
|
||||
"fieldname": "use_batchwise_valuation",
|
||||
"fieldtype": "Check",
|
||||
|
||||
@@ -218,15 +218,16 @@ class SerialBatchBundle:
|
||||
).validate_serial_and_batch_inventory()
|
||||
|
||||
def post_process(self):
|
||||
if not self.sle.serial_and_batch_bundle:
|
||||
if not self.sle.serial_and_batch_bundle and not self.sle.serial_no and not self.sle.batch_no:
|
||||
return
|
||||
|
||||
docstatus = frappe.get_cached_value(
|
||||
"Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "docstatus"
|
||||
)
|
||||
if self.sle.serial_and_batch_bundle:
|
||||
docstatus = frappe.get_cached_value(
|
||||
"Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "docstatus"
|
||||
)
|
||||
|
||||
if docstatus != 1:
|
||||
self.submit_serial_and_batch_bundle()
|
||||
if docstatus != 1:
|
||||
self.submit_serial_and_batch_bundle()
|
||||
|
||||
if self.item_details.has_serial_no == 1:
|
||||
self.set_warehouse_and_status_in_serial_nos()
|
||||
@@ -249,7 +250,12 @@ class SerialBatchBundle:
|
||||
doc.submit()
|
||||
|
||||
def set_warehouse_and_status_in_serial_nos(self):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos as get_parsed_serial_nos
|
||||
|
||||
serial_nos = get_serial_nos(self.sle.serial_and_batch_bundle)
|
||||
if not self.sle.serial_and_batch_bundle and self.sle.serial_no:
|
||||
serial_nos = get_parsed_serial_nos(self.sle.serial_no)
|
||||
|
||||
warehouse = self.warehouse if self.sle.actual_qty > 0 else None
|
||||
|
||||
if not serial_nos:
|
||||
@@ -263,7 +269,14 @@ class SerialBatchBundle:
|
||||
(
|
||||
frappe.qb.update(sn_table)
|
||||
.set(sn_table.warehouse, warehouse)
|
||||
.set(sn_table.status, "Active" if warehouse else status)
|
||||
.set(
|
||||
sn_table.status,
|
||||
"Active"
|
||||
if warehouse
|
||||
else status
|
||||
if (sn_table.purchase_document_no != self.sle.voucher_no and self.sle.is_cancelled != 1)
|
||||
else "Inactive",
|
||||
)
|
||||
.where(sn_table.name.isin(serial_nos))
|
||||
).run()
|
||||
|
||||
@@ -290,6 +303,8 @@ class SerialBatchBundle:
|
||||
from erpnext.stock.doctype.batch.batch import get_available_batches
|
||||
|
||||
batches = get_batch_nos(self.sle.serial_and_batch_bundle)
|
||||
if not self.sle.serial_and_batch_bundle and self.sle.batch_no:
|
||||
batches = frappe._dict({self.sle.batch_no: self.sle.actual_qty})
|
||||
|
||||
batches_qty = get_available_batches(
|
||||
frappe._dict(
|
||||
@@ -312,13 +327,35 @@ def get_serial_nos(serial_and_batch_bundle, serial_nos=None):
|
||||
if serial_nos:
|
||||
filters["serial_no"] = ("in", serial_nos)
|
||||
|
||||
entries = frappe.get_all("Serial and Batch Entry", fields=["serial_no"], filters=filters)
|
||||
entries = frappe.get_all(
|
||||
"Serial and Batch Entry", fields=["serial_no"], filters=filters, order_by="idx"
|
||||
)
|
||||
if not entries:
|
||||
return []
|
||||
|
||||
return [d.serial_no for d in entries if d.serial_no]
|
||||
|
||||
|
||||
def get_batches_from_bundle(serial_and_batch_bundle, batches=None):
|
||||
if not serial_and_batch_bundle:
|
||||
return []
|
||||
|
||||
filters = {"parent": serial_and_batch_bundle, "batch_no": ("is", "set")}
|
||||
if isinstance(serial_and_batch_bundle, list):
|
||||
filters = {"parent": ("in", serial_and_batch_bundle)}
|
||||
|
||||
if batches:
|
||||
filters["batch_no"] = ("in", batches)
|
||||
|
||||
entries = frappe.get_all(
|
||||
"Serial and Batch Entry", fields=["batch_no", "qty"], filters=filters, order_by="idx", as_list=1
|
||||
)
|
||||
if not entries:
|
||||
return frappe._dict({})
|
||||
|
||||
return frappe._dict(entries)
|
||||
|
||||
|
||||
def get_serial_nos_from_bundle(serial_and_batch_bundle, serial_nos=None):
|
||||
return get_serial_nos(serial_and_batch_bundle, serial_nos=serial_nos)
|
||||
|
||||
|
||||
@@ -210,6 +210,11 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
|
||||
sle.allow_negative_stock = allow_negative_stock
|
||||
sle.via_landed_cost_voucher = via_landed_cost_voucher
|
||||
sle.submit()
|
||||
|
||||
# Added to handle the case when the stock ledger entry is created from the repostig
|
||||
if args.get("creation_time") and args.get("voucher_type") == "Stock Reconciliation":
|
||||
sle.db_set("creation", args.get("creation_time"))
|
||||
|
||||
return sle
|
||||
|
||||
|
||||
@@ -696,9 +701,11 @@ class update_entries_after(object):
|
||||
|
||||
if (
|
||||
sle.voucher_type == "Stock Reconciliation"
|
||||
and (sle.batch_no or (sle.has_batch_no and sle.serial_and_batch_bundle))
|
||||
and (
|
||||
sle.batch_no or (sle.has_batch_no and sle.serial_and_batch_bundle and not sle.has_serial_no)
|
||||
)
|
||||
and sle.voucher_detail_no
|
||||
and sle.actual_qty < 0
|
||||
and not self.args.get("sle_id")
|
||||
):
|
||||
self.reset_actual_qty_for_stock_reco(sle)
|
||||
|
||||
@@ -765,27 +772,22 @@ class update_entries_after(object):
|
||||
self.update_outgoing_rate_on_transaction(sle)
|
||||
|
||||
def reset_actual_qty_for_stock_reco(self, sle):
|
||||
if sle.serial_and_batch_bundle:
|
||||
current_qty = frappe.get_cached_value(
|
||||
"Serial and Batch Bundle", sle.serial_and_batch_bundle, "total_qty"
|
||||
doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
|
||||
doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
|
||||
|
||||
if sle.actual_qty < 0:
|
||||
sle.actual_qty = (
|
||||
flt(frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"))
|
||||
* -1
|
||||
)
|
||||
|
||||
if current_qty is not None:
|
||||
current_qty = abs(current_qty)
|
||||
else:
|
||||
current_qty = frappe.get_cached_value(
|
||||
"Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"
|
||||
)
|
||||
|
||||
if current_qty:
|
||||
sle.actual_qty = current_qty * -1
|
||||
elif current_qty == 0:
|
||||
sle.is_cancelled = 1
|
||||
if abs(sle.actual_qty) == 0.0:
|
||||
sle.is_cancelled = 1
|
||||
|
||||
def calculate_valuation_for_serial_batch_bundle(self, sle):
|
||||
doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
|
||||
|
||||
doc.set_incoming_rate(save=True)
|
||||
doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock)
|
||||
doc.calculate_qty_and_amount(save=True)
|
||||
|
||||
self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount)
|
||||
@@ -1472,6 +1474,7 @@ def get_valuation_rate(
|
||||
currency=None,
|
||||
company=None,
|
||||
raise_error_if_no_rate=True,
|
||||
batch_no=None,
|
||||
serial_and_batch_bundle=None,
|
||||
):
|
||||
|
||||
@@ -1480,6 +1483,25 @@ def get_valuation_rate(
|
||||
if not company:
|
||||
company = frappe.get_cached_value("Warehouse", warehouse, "company")
|
||||
|
||||
if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
|
||||
table = frappe.qb.DocType("Stock Ledger Entry")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(Sum(table.stock_value_difference) / Sum(table.actual_qty))
|
||||
.where(
|
||||
(table.item_code == item_code)
|
||||
& (table.warehouse == warehouse)
|
||||
& (table.batch_no == batch_no)
|
||||
& (table.is_cancelled == 0)
|
||||
& (table.voucher_no != voucher_no)
|
||||
& (table.voucher_type != voucher_type)
|
||||
)
|
||||
)
|
||||
|
||||
last_valuation_rate = query.run()
|
||||
if last_valuation_rate:
|
||||
return flt(last_valuation_rate[0][0])
|
||||
|
||||
# Get moving average rate of a specific batch number
|
||||
if warehouse and serial_and_batch_bundle:
|
||||
batch_obj = BatchNoValuation(
|
||||
@@ -1574,8 +1596,6 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
|
||||
next_stock_reco_detail = get_next_stock_reco(args)
|
||||
if next_stock_reco_detail:
|
||||
detail = next_stock_reco_detail[0]
|
||||
if detail.batch_no or (detail.serial_and_batch_bundle and detail.has_batch_no):
|
||||
regenerate_sle_for_batch_stock_reco(detail)
|
||||
|
||||
# add condition to update SLEs before this date & time
|
||||
datetime_limit_condition = get_datetime_limit_condition(detail)
|
||||
@@ -1604,16 +1624,6 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
|
||||
validate_negative_qty_in_future_sle(args, allow_negative_stock)
|
||||
|
||||
|
||||
def regenerate_sle_for_batch_stock_reco(detail):
|
||||
doc = frappe.get_cached_doc("Stock Reconciliation", detail.voucher_no)
|
||||
doc.recalculate_current_qty(detail.item_code, detail.batch_no)
|
||||
|
||||
if not frappe.db.exists(
|
||||
"Repost Item Valuation", {"voucher_no": doc.name, "status": "Queued", "docstatus": "1"}
|
||||
):
|
||||
doc.repost_future_sle_and_gle(force=True)
|
||||
|
||||
|
||||
def get_stock_reco_qty_shift(args):
|
||||
stock_reco_qty_shift = 0
|
||||
if args.get("is_cancelled"):
|
||||
|
||||
@@ -7,6 +7,7 @@ from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import update_status as update_po_status
|
||||
from erpnext.controllers.subcontracting_controller import SubcontractingController
|
||||
from erpnext.stock.stock_balance import update_bin_qty
|
||||
from erpnext.stock.utils import get_bin
|
||||
@@ -308,6 +309,9 @@ class SubcontractingOrder(SubcontractingController):
|
||||
"Subcontracting Order", self.name, "status", status, update_modified=update_modified
|
||||
)
|
||||
|
||||
if status == "Closed":
|
||||
update_po_status("Closed", self.purchase_order)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_subcontracting_receipt(source_name, target_doc=None):
|
||||
|
||||
@@ -167,13 +167,13 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
)
|
||||
self.update_status_updater_args()
|
||||
self.update_prevdoc_status()
|
||||
self.delete_auto_created_batches()
|
||||
self.set_consumed_qty_in_subcontract_order()
|
||||
self.set_subcontracting_order_status()
|
||||
self.update_stock_ledger()
|
||||
self.make_gl_entries_on_cancel()
|
||||
self.repost_future_sle_and_gle()
|
||||
self.update_status()
|
||||
self.delete_auto_created_batches()
|
||||
|
||||
def validate_items_qty(self):
|
||||
for item in self.items:
|
||||
|
||||
@@ -953,6 +953,91 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
||||
|
||||
scr.submit()
|
||||
|
||||
def test_subcontracting_receipt_cancel_with_batch(self):
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
|
||||
# Step - 1: Set Backflush Based On as "BOM"
|
||||
set_backflush_based_on("BOM")
|
||||
|
||||
# Step - 2: Create FG and RM Items
|
||||
fg_item = make_item(
|
||||
properties={"is_stock_item": 1, "is_sub_contracted_item": 1, "has_batch_no": 1}
|
||||
).name
|
||||
rm_item1 = make_item(properties={"is_stock_item": 1}).name
|
||||
rm_item2 = make_item(properties={"is_stock_item": 1}).name
|
||||
make_item("Subcontracted Service Item Test For Batch 1", {"is_stock_item": 0})
|
||||
|
||||
# Step - 3: Create BOM for FG Item
|
||||
bom = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2])
|
||||
for rm_item in bom.items:
|
||||
self.assertEqual(rm_item.rate, 0)
|
||||
self.assertEqual(rm_item.amount, 0)
|
||||
bom = bom.name
|
||||
|
||||
# Step - 4: Create PO and SCO
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item Test For Batch 1",
|
||||
"qty": 100,
|
||||
"rate": 100,
|
||||
"fg_item": fg_item,
|
||||
"fg_item_qty": 100,
|
||||
},
|
||||
]
|
||||
sco = get_subcontracting_order(service_items=service_items)
|
||||
for rm_item in sco.supplied_items:
|
||||
self.assertEqual(rm_item.rate, 0)
|
||||
self.assertEqual(rm_item.amount, 0)
|
||||
|
||||
# Step - 5: Inward Raw Materials
|
||||
rm_items = get_rm_items(sco.supplied_items)
|
||||
for rm_item in rm_items:
|
||||
rm_item["rate"] = 100
|
||||
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||
|
||||
# Step - 6: Transfer RM's to Subcontractor
|
||||
se = make_stock_transfer_entry(
|
||||
sco_no=sco.name,
|
||||
rm_items=rm_items,
|
||||
itemwise_details=copy.deepcopy(itemwise_details),
|
||||
)
|
||||
for item in se.items:
|
||||
self.assertEqual(item.qty, 100)
|
||||
self.assertEqual(item.basic_rate, 100)
|
||||
self.assertEqual(item.amount, item.qty * item.basic_rate)
|
||||
|
||||
batch_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"item": fg_item,
|
||||
"batch_id": frappe.generate_hash(length=10),
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
serial_batch_bundle = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Serial and Batch Bundle",
|
||||
"item_code": fg_item,
|
||||
"warehouse": sco.items[0].warehouse,
|
||||
"has_batch_no": 1,
|
||||
"type_of_transaction": "Inward",
|
||||
"voucher_type": "Subcontracting Receipt",
|
||||
"entries": [{"batch_no": batch_doc.name, "qty": 100}],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
# Step - 7: Create Subcontracting Receipt
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.items[0].serial_and_batch_bundle = serial_batch_bundle.name
|
||||
scr.save()
|
||||
scr.submit()
|
||||
scr.load_from_db()
|
||||
|
||||
# Step - 8: Cancel Subcontracting Receipt
|
||||
scr.cancel()
|
||||
self.assertTrue(scr.docstatus == 2)
|
||||
|
||||
@change_settings("Buying Settings", {"auto_create_purchase_receipt": 1})
|
||||
def test_auto_create_purchase_receipt(self):
|
||||
fg_item = "Subcontracted Item SA1"
|
||||
|
||||
@@ -58,7 +58,9 @@ frappe.ui.form.on("Issue", {
|
||||
|
||||
frappe.call("erpnext.support.doctype.service_level_agreement.service_level_agreement.reset_service_level_agreement", {
|
||||
reason: values.reason,
|
||||
user: frappe.session.user_email
|
||||
user: frappe.session.user_email,
|
||||
doctype: frm.doc.doctype,
|
||||
docname: frm.doc.name,
|
||||
}, () => {
|
||||
reset_sla.enable_primary_action();
|
||||
frm.refresh();
|
||||
|
||||
@@ -774,10 +774,12 @@ def get_response_and_resolution_duration(doc):
|
||||
return priority
|
||||
|
||||
|
||||
def reset_service_level_agreement(doc, reason, user):
|
||||
@frappe.whitelist()
|
||||
def reset_service_level_agreement(doctype: str, docname: str, reason, user):
|
||||
if not frappe.db.get_single_value("Support Settings", "allow_resetting_service_level_agreement"):
|
||||
frappe.throw(_("Allow Resetting Service Level Agreement from Support Settings."))
|
||||
|
||||
doc = frappe.get_doc(doctype, docname)
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Comment",
|
||||
|
||||
@@ -26,6 +26,26 @@
|
||||
{{ render_homepage_section(homepage.hero_section_doc) }}
|
||||
{% endif %}
|
||||
|
||||
{% if homepage.products %}
|
||||
<section class="container section-products my-5">
|
||||
<h3>{{ _('Products') }}</h3>
|
||||
|
||||
<div class="row">
|
||||
{% for item in homepage.products %}
|
||||
<div class="col-md-4 mb-4">
|
||||
<div class="card h-100 justify-content-between">
|
||||
<img class="card-img-top website-image-extra-large" src="{{ item.image }}" loading="lazy" alt="{{ item.item_name }}"></img>
|
||||
<div class="card-body flex-grow-0">
|
||||
<h5 class="card-title">{{ item.item_name }}</h5>
|
||||
<a href="{{ item.route }}" class="card-link">{{ _('More details') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if blogs %}
|
||||
<section class="container my-5">
|
||||
<h3>{{ _('Publications') }}</h3>
|
||||
|
||||
Reference in New Issue
Block a user