Merge pull request #40456 from nabinhait/pi-optimization-dev
perf: Performance optimization for Purchase Invoice submission
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
index 30e564c..6728fea 100644
--- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
@@ -149,6 +149,9 @@
import_file = ImportFile("Bank Transaction", file=file, import_type="Insert New Records")
data = parse_data_from_template(import_file.raw_data)
+ # Importer expects 'Data Import' class, which has 'payload_count' attribute
+ if not data_import.get("payload_count"):
+ data_import.payload_count = len(data) - 1
if import_file_path:
add_bank_account(data, bank_account)
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index 5e17881..4246ba5 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -56,17 +56,17 @@
Bank Transaction should be on the same currency as the Bank Account.
"""
if self.currency and self.bank_account:
- account = frappe.get_cached_value("Bank Account", self.bank_account, "account")
- account_currency = frappe.get_cached_value("Account", account, "account_currency")
+ if account := frappe.get_cached_value("Bank Account", self.bank_account, "account"):
+ account_currency = frappe.get_cached_value("Account", account, "account_currency")
- if self.currency != account_currency:
- frappe.throw(
- _(
- "Transaction currency: {0} cannot be different from Bank Account({1}) currency: {2}"
- ).format(
- frappe.bold(self.currency), frappe.bold(self.bank_account), frappe.bold(account_currency)
+ if self.currency != account_currency:
+ frappe.throw(
+ _(
+ "Transaction currency: {0} cannot be different from Bank Account({1}) currency: {2}"
+ ).format(
+ frappe.bold(self.currency), frappe.bold(self.bank_account), frappe.bold(account_currency)
+ )
)
- )
def set_status(self):
if self.docstatus == 2:
diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js
index d931f62..ad68352 100644
--- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js
+++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js
@@ -3,22 +3,36 @@
frappe.ui.form.on("Currency Exchange Settings", {
service_provider: function (frm) {
- if (frm.doc.service_provider == "exchangerate.host") {
- let result = ["result"];
- let params = {
- date: "{transaction_date}",
- from: "{from_currency}",
- to: "{to_currency}",
- };
- add_param(frm, "https://api.exchangerate.host/convert", params, result);
- } else if (frm.doc.service_provider == "frankfurter.app") {
- let result = ["rates", "{to_currency}"];
- let params = {
- base: "{from_currency}",
- symbols: "{to_currency}",
- };
- add_param(frm, "https://frankfurter.app/{transaction_date}", params, result);
- }
+ frm.call({
+ method: "erpnext.accounts.doctype.currency_exchange_settings.currency_exchange_settings.get_api_endpoint",
+ args: {
+ service_provider: frm.doc.service_provider,
+ use_http: frm.doc.use_http,
+ },
+ callback: function (r) {
+ if (r && r.message) {
+ if (frm.doc.service_provider == "exchangerate.host") {
+ let result = ["result"];
+ let params = {
+ date: "{transaction_date}",
+ from: "{from_currency}",
+ to: "{to_currency}",
+ };
+ add_param(frm, r.message, params, result);
+ } else if (frm.doc.service_provider == "frankfurter.app") {
+ let result = ["rates", "{to_currency}"];
+ let params = {
+ base: "{from_currency}",
+ symbols: "{to_currency}",
+ };
+ add_param(frm, r.message, params, result);
+ }
+ }
+ },
+ });
+ },
+ use_http: function (frm) {
+ frm.trigger("service_provider");
},
});
diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
index df232a5..bd90b8a 100644
--- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
+++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
@@ -9,6 +9,7 @@
"disabled",
"service_provider",
"api_endpoint",
+ "use_http",
"access_key",
"url",
"column_break_3",
@@ -91,12 +92,19 @@
"fieldname": "access_key",
"fieldtype": "Data",
"label": "Access Key"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: doc.service_provider != \"Custom\"",
+ "fieldname": "use_http",
+ "fieldtype": "Check",
+ "label": "Use HTTP Protocol"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2023-10-04 15:30:25.333860",
+ "modified": "2024-03-18 08:32:26.895076",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Currency Exchange Settings",
diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py
index 3393d41..b8817c6 100644
--- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py
+++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py
@@ -31,6 +31,7 @@
result_key: DF.Table[CurrencyExchangeSettingsResult]
service_provider: DF.Literal["frankfurter.app", "exchangerate.host", "Custom"]
url: DF.Data | None
+ use_http: DF.Check
# end: auto-generated types
def validate(self):
@@ -53,7 +54,7 @@
self.set("result_key", [])
self.set("req_params", [])
- self.api_endpoint = "https://api.exchangerate.host/convert"
+ self.api_endpoint = get_api_endpoint(self.service_provider, self.use_http)
self.append("result_key", {"key": "result"})
self.append("req_params", {"key": "access_key", "value": self.access_key})
self.append("req_params", {"key": "amount", "value": "1"})
@@ -64,7 +65,7 @@
self.set("result_key", [])
self.set("req_params", [])
- self.api_endpoint = "https://frankfurter.app/{transaction_date}"
+ self.api_endpoint = get_api_endpoint(self.service_provider, self.use_http)
self.append("result_key", {"key": "rates"})
self.append("result_key", {"key": "{to_currency}"})
self.append("req_params", {"key": "base", "value": "{from_currency}"})
@@ -103,3 +104,19 @@
frappe.throw(_("Returned exchange rate is neither integer not float."))
self.url = response.url
+
+
+@frappe.whitelist()
+def get_api_endpoint(service_provider: str = None, use_http: bool = False):
+ if service_provider and service_provider in ["exchangerate.host", "frankfurter.app"]:
+ if service_provider == "exchangerate.host":
+ api = "api.exchangerate.host/convert"
+ elif service_provider == "frankfurter.app":
+ api = "frankfurter.app/{transaction_date}"
+
+ protocol = "https://"
+ if use_http:
+ protocol = "http://"
+
+ return protocol + api
+ return None
diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
index 8be09db..29732ef 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
@@ -628,21 +628,21 @@
if account_balance and (
account_balance[0].balance or account_balance[0].balance_in_account_currency
):
- account_with_new_balance = ExchangeRateRevaluation.calculate_new_account_balance(
+ if account_with_new_balance := ExchangeRateRevaluation.calculate_new_account_balance(
company, posting_date, account_balance
- )
- row = account_with_new_balance[0]
- account_details.update(
- {
- "balance_in_base_currency": row["balance_in_base_currency"],
- "balance_in_account_currency": row["balance_in_account_currency"],
- "current_exchange_rate": row["current_exchange_rate"],
- "new_exchange_rate": row["new_exchange_rate"],
- "new_balance_in_base_currency": row["new_balance_in_base_currency"],
- "new_balance_in_account_currency": row["new_balance_in_account_currency"],
- "zero_balance": row["zero_balance"],
- "gain_loss": row["gain_loss"],
- }
- )
+ ):
+ row = account_with_new_balance[0]
+ account_details.update(
+ {
+ "balance_in_base_currency": row["balance_in_base_currency"],
+ "balance_in_account_currency": row["balance_in_account_currency"],
+ "current_exchange_rate": row["current_exchange_rate"],
+ "new_exchange_rate": row["new_exchange_rate"],
+ "new_balance_in_base_currency": row["new_balance_in_base_currency"],
+ "new_balance_in_account_currency": row["new_balance_in_account_currency"],
+ "zero_balance": row["zero_balance"],
+ "gain_loss": row["gain_loss"],
+ }
+ )
return account_details
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index ab50c38..961ee20 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -395,10 +395,6 @@
return {
query: "erpnext.controllers.queries.employee_query",
};
- } else if (frm.doc.party_type == "Customer") {
- return {
- query: "erpnext.controllers.queries.customer_query",
- };
}
});
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 95bb188..b4d1d39 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -489,7 +489,9 @@
ref_details = get_reference_details(
d.reference_doctype, d.reference_name, self.party_account_currency
)
- if ref_exchange_rate:
+
+ # Only update exchange rate when the reference is Journal Entry
+ if ref_exchange_rate and d.reference_doctype == "Journal Entry":
ref_details.update({"exchange_rate": ref_exchange_rate})
for field, value in ref_details.items():
@@ -611,9 +613,9 @@
def get_valid_reference_doctypes(self):
if self.party_type == "Customer":
- return ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning")
+ return ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning", "Payment Entry")
elif self.party_type == "Supplier":
- return ("Purchase Order", "Purchase Invoice", "Journal Entry")
+ return ("Purchase Order", "Purchase Invoice", "Journal Entry", "Payment Entry")
elif self.party_type == "Shareholder":
return ("Journal Entry",)
elif self.party_type == "Employee":
@@ -1279,6 +1281,7 @@
"Journal Entry",
"Sales Order",
"Purchase Order",
+ "Payment Entry",
):
self.add_advance_gl_for_reference(gl_entries, ref)
@@ -1301,7 +1304,9 @@
if getdate(posting_date) < getdate(self.posting_date):
posting_date = self.posting_date
- dr_or_cr = "credit" if invoice.reference_doctype in ["Sales Invoice", "Sales Order"] else "debit"
+ dr_or_cr = (
+ "credit" if invoice.reference_doctype in ["Sales Invoice", "Payment Entry"] else "debit"
+ )
args_dict["account"] = invoice.account
args_dict[dr_or_cr] = invoice.allocated_amount
args_dict[dr_or_cr + "_in_account_currency"] = invoice.allocated_amount
@@ -1751,7 +1756,7 @@
outstanding_invoices = get_outstanding_invoices(
args.get("party_type"),
args.get("party"),
- party_account,
+ [party_account],
common_filter=common_filter,
posting_date=posting_and_due_date,
min_outstanding=args.get("outstanding_amt_greater_than"),
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index 5a014b8..6323e4c 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -1514,6 +1514,168 @@
for field in ["account", "debit", "credit"]:
self.assertEqual(self.expected_gle[row][field], gl_entries[row][field])
+ def test_reverse_payment_reconciliation(self):
+ customer = create_customer(frappe.generate_hash(length=10), "INR")
+ pe = create_payment_entry(
+ party_type="Customer",
+ party=customer,
+ payment_type="Receive",
+ paid_from="Debtors - _TC",
+ paid_to="_Test Cash - _TC",
+ )
+ pe.submit()
+
+ reverse_pe = create_payment_entry(
+ party_type="Customer",
+ party=customer,
+ payment_type="Pay",
+ paid_from="_Test Cash - _TC",
+ paid_to="Debtors - _TC",
+ )
+ reverse_pe.submit()
+
+ pr = frappe.get_doc("Payment Reconciliation")
+ pr.company = "_Test Company"
+ pr.party_type = "Customer"
+ pr.party = customer
+ pr.receivable_payable_account = "Debtors - _TC"
+ pr.get_unreconciled_entries()
+ self.assertEqual(len(pr.invoices), 1)
+ self.assertEqual(len(pr.payments), 1)
+
+ self.assertEqual(reverse_pe.name, pr.invoices[0].invoice_number)
+ self.assertEqual(pe.name, pr.payments[0].reference_name)
+
+ invoices = [x.as_dict() for x in pr.invoices]
+ payments = [pr.payments[0].as_dict()]
+ pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+ pr.reconcile()
+ self.assertEqual(len(pr.invoices), 0)
+ self.assertEqual(len(pr.payments), 0)
+
+ def test_advance_reverse_payment_reconciliation(self):
+ from erpnext.accounts.doctype.account.test_account import create_account
+
+ company = "_Test Company"
+ customer = create_customer(frappe.generate_hash(length=10), "INR")
+ advance_account = create_account(
+ parent_account="Current Assets - _TC",
+ account_name="Advances Received",
+ company=company,
+ account_type="Receivable",
+ )
+
+ frappe.db.set_value(
+ "Company",
+ company,
+ {
+ "book_advance_payments_in_separate_party_account": 1,
+ "default_advance_received_account": advance_account,
+ },
+ )
+ # Reverse Payment(essentially an Invoice)
+ reverse_pe = create_payment_entry(
+ party_type="Customer",
+ party=customer,
+ payment_type="Pay",
+ paid_from="_Test Cash - _TC",
+ paid_to=advance_account,
+ )
+ reverse_pe.save() # use save() to trigger set_liability_account()
+ reverse_pe.submit()
+
+ # Advance Payment
+ pe = create_payment_entry(
+ party_type="Customer",
+ party=customer,
+ payment_type="Receive",
+ paid_from=advance_account,
+ paid_to="_Test Cash - _TC",
+ )
+ pe.save() # use save() to trigger set_liability_account()
+ pe.submit()
+
+ # Partially reconcile advance against invoice
+ pr = frappe.get_doc("Payment Reconciliation")
+ pr.company = company
+ pr.party_type = "Customer"
+ pr.party = customer
+ pr.receivable_payable_account = "Debtors - _TC"
+ pr.default_advance_account = advance_account
+ pr.get_unreconciled_entries()
+
+ self.assertEqual(len(pr.invoices), 1)
+ self.assertEqual(len(pr.payments), 1)
+
+ invoices = [x.as_dict() for x in pr.get("invoices")]
+ payments = [x.as_dict() for x in pr.get("payments")]
+ pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+ pr.allocation[0].allocated_amount = 400
+ pr.reconcile()
+
+ # assert General and Payment Ledger entries post partial reconciliation
+ self.expected_gle = [
+ {"account": "Debtors - _TC", "debit": 0.0, "credit": 400.0},
+ {"account": advance_account, "debit": 400.0, "credit": 0.0},
+ {"account": advance_account, "debit": 0.0, "credit": 1000.0},
+ {"account": "_Test Cash - _TC", "debit": 1000.0, "credit": 0.0},
+ ]
+ self.expected_ple = [
+ {
+ "account": advance_account,
+ "voucher_no": pe.name,
+ "against_voucher_no": pe.name,
+ "amount": -1000.0,
+ },
+ {
+ "account": "Debtors - _TC",
+ "voucher_no": pe.name,
+ "against_voucher_no": reverse_pe.name,
+ "amount": -400.0,
+ },
+ {
+ "account": advance_account,
+ "voucher_no": pe.name,
+ "against_voucher_no": pe.name,
+ "amount": 400.0,
+ },
+ ]
+ self.voucher_no = pe.name
+ self.check_gl_entries()
+ self.check_pl_entries()
+
+ # Unreconcile
+ unrecon = (
+ frappe.get_doc(
+ {
+ "doctype": "Unreconcile Payment",
+ "company": company,
+ "voucher_type": pe.doctype,
+ "voucher_no": pe.name,
+ "allocations": [{"reference_doctype": reverse_pe.doctype, "reference_name": reverse_pe.name}],
+ }
+ )
+ .save()
+ .submit()
+ )
+
+ # assert General and Payment Ledger entries post unreconciliation
+ self.expected_gle = [
+ {"account": advance_account, "debit": 0.0, "credit": 1000.0},
+ {"account": "_Test Cash - _TC", "debit": 1000.0, "credit": 0.0},
+ ]
+ self.expected_ple = [
+ {
+ "account": advance_account,
+ "voucher_no": pe.name,
+ "against_voucher_no": pe.name,
+ "amount": -1000.0,
+ },
+ ]
+ self.voucher_no = pe.name
+ self.check_gl_entries()
+ self.check_pl_entries()
+
def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry")
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 972ce26..dcb1a16 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -340,10 +340,15 @@
self.build_qb_filter_conditions(get_invoices=True)
+ accounts = [self.receivable_payable_account]
+
+ if self.default_advance_account:
+ accounts.append(self.default_advance_account)
+
non_reconciled_invoices = get_outstanding_invoices(
self.party_type,
self.party,
- self.receivable_payable_account,
+ accounts,
common_filter=self.common_filter_conditions,
posting_date=self.ple_posting_date_filter,
min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None,
diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
index 301e6ef..1d20a5b 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
@@ -1130,6 +1130,17 @@
self.assertEqual(pr.allocation[0].allocated_amount, 85)
self.assertEqual(pr.allocation[0].difference_amount, 0)
+ pr.reconcile()
+ si.reload()
+ self.assertEqual(si.outstanding_amount, 0)
+ # No Exchange Gain/Loss journal should be generated
+ exc_gain_loss_journals = frappe.db.get_all(
+ "Journal Entry Account",
+ filters={"reference_type": si.doctype, "reference_name": si.name, "docstatus": 1},
+ fields=["parent"],
+ )
+ self.assertEqual(exc_gain_loss_journals, [])
+
def test_reconciliation_purchase_invoice_against_return(self):
self.supplier = "_Test Supplier USD"
pi = self.create_purchase_invoice(qty=5, rate=50, do_not_submit=True)
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
index 5307ccb..81ebf97 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
@@ -89,10 +89,11 @@
<table class="table table-bordered">
<thead>
<tr>
- <th style="width: 25%">30 Days</th>
- <th style="width: 25%">60 Days</th>
- <th style="width: 25%">90 Days</th>
- <th style="width: 25%">120 Days</th>
+ <th style="width: 20%">0 - 30 Days</th>
+ <th style="width: 20%">30 - 60 Days</th>
+ <th style="width: 20%">60 - 90 Days</th>
+ <th style="width: 20%">90 - 120 Days</th>
+ <th style="width: 20%">Above 120 Days</th>
</tr>
</thead>
<tbody>
@@ -101,6 +102,7 @@
<td>{{ frappe.utils.fmt_money(ageing.range2, currency=filters.presentation_currency) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range3, currency=filters.presentation_currency) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range4, currency=filters.presentation_currency) }}</td>
+ <td>{{ frappe.utils.fmt_money(ageing.range5, currency=filters.presentation_currency) }}</td>
</tr>
</tbody>
</table>
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index ac14d98..37b2752 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -2179,7 +2179,8 @@
"description": "Credit Note will update it's own outstanding amount, even if \"Return Against\" is specified.",
"fieldname": "update_outstanding_for_self",
"fieldtype": "Check",
- "label": "Update Outstanding for Self"
+ "label": "Update Outstanding for Self",
+ "no_copy": 1
}
],
"icon": "fa fa-file-text",
@@ -2192,7 +2193,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2024-03-11 14:20:34.874192",
+ "modified": "2024-03-15 16:44:17.778370",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index c8a35eb..7e3eec5 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1588,6 +1588,12 @@
self.assertEqual(frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount"), -1000)
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 2500)
+ def test_zero_qty_return_invoice_with_stock_effect(self):
+ cr_note = create_sales_invoice(qty=-1, rate=300, is_return=1, do_not_submit=True)
+ cr_note.update_stock = True
+ cr_note.items[0].qty = 0
+ self.assertRaises(frappe.ValidationError, cr_note.save)
+
def test_return_invoice_with_account_mismatch(self):
debtors2 = create_account(
parent_account="Accounts Receivable - _TC",
@@ -3945,7 +3951,6 @@
)
supplier.append("companies", {"company": allowed_to_interact_with})
-
supplier.insert()
supplier_name = supplier.name
else:
diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
index a0f8af5..de49139 100644
--- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
@@ -182,8 +182,10 @@
}
# check invoice grand total and invoiced column's value for 3 payment terms
- si = self.create_sales_invoice(no_payment_schedule=True)
- name = si.name
+ si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
+ si.set_posting_time = True
+ si.posting_date = add_days(today(), -1)
+ si.save().submit()
report = execute(filters)
@@ -207,30 +209,42 @@
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
cr_note = self.create_credit_note(si.name, do_not_submit=True)
- cr_note.posting_date = add_days(today(), 1)
cr_note.update_outstanding_for_self = True
cr_note.save().submit()
report = execute(filters)
expected_data_after_credit_note = [
- [100.0, 100.0, 40.0, 0.0, 60.0, self.debit_to],
- [0, 0, 100.0, 0.0, -100.0, self.debit_to],
+ [100.0, 100.0, 40.0, 0.0, 60.0, si.name],
+ [0, 0, 100.0, 0.0, -100.0, cr_note.name],
]
self.assertEqual(len(report[1]), 2)
- for i in range(2):
- row = report[1][i - 1]
- # row = report[1][0]
- self.assertEqual(
- expected_data_after_credit_note[i - 1],
- [
- row.invoice_grand_total,
- row.invoiced,
- row.paid,
- row.credit_note,
- row.outstanding,
- row.party_account,
- ],
- )
+ si_row = [
+ [
+ row.invoice_grand_total,
+ row.invoiced,
+ row.paid,
+ row.credit_note,
+ row.outstanding,
+ row.voucher_no,
+ ]
+ for row in report[1]
+ if row.voucher_no == si.name
+ ][0]
+
+ cr_note_row = [
+ [
+ row.invoice_grand_total,
+ row.invoiced,
+ row.paid,
+ row.credit_note,
+ row.outstanding,
+ row.voucher_no,
+ ]
+ for row in report[1]
+ if row.voucher_no == cr_note.name
+ ][0]
+ self.assertEqual(expected_data_after_credit_note[0], si_row)
+ self.assertEqual(expected_data_after_credit_note[1], cr_note_row)
def test_payment_againt_po_in_receivable_report(self):
"""
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 0755f2e..02012ad 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -1027,7 +1027,7 @@
if account:
root_type, account_type = frappe.get_cached_value(
- "Account", account, ["root_type", "account_type"]
+ "Account", account[0], ["root_type", "account_type"]
)
party_account_type = "Receivable" if root_type == "Asset" else "Payable"
party_account_type = account_type or party_account_type
@@ -1038,7 +1038,7 @@
common_filter = common_filter or []
common_filter.append(ple.account_type == party_account_type)
- common_filter.append(ple.account == account)
+ common_filter.append(ple.account.isin(account))
common_filter.append(ple.party_type == party_type)
common_filter.append(ple.party == party)
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index 2bb602c..e62d22b 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -509,7 +509,6 @@
target: me.frm,
setters: {
schedule_date: undefined,
- status: undefined,
},
get_query_filters: {
material_request_type: "Purchase",
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
index 272d077..6b10df8 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
@@ -518,16 +518,15 @@
callback: load_suppliers,
});
} else if (args.supplier_group) {
- return frappe.call({
- method: "frappe.client.get_list",
- args: {
- doctype: "Supplier",
+ frappe.db
+ .get_list("Supplier", {
+ filters: { supplier_group: args.supplier_group },
+ limit: 100,
order_by: "name",
- fields: ["name"],
- filters: [["Supplier", "supplier_group", "=", args.supplier_group]],
- },
- callback: load_suppliers,
- });
+ })
+ .then((r) => {
+ load_suppliers({ message: r });
+ });
}
},
});
diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json
index 60dd54c..3dae044 100644
--- a/erpnext/buying/doctype/supplier/supplier.json
+++ b/erpnext/buying/doctype/supplier/supplier.json
@@ -485,7 +485,7 @@
"link_fieldname": "party"
}
],
- "modified": "2023-10-19 16:55:15.148325",
+ "modified": "2024-03-13 11:14:06.516519",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",
@@ -544,7 +544,7 @@
}
],
"quick_entry": 1,
- "search_fields": "supplier_name, supplier_group",
+ "search_fields": "supplier_group",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "ASC",
diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py
index 350a25f..55974ea 100644
--- a/erpnext/buying/doctype/supplier/test_supplier.py
+++ b/erpnext/buying/doctype/supplier/test_supplier.py
@@ -154,44 +154,6 @@
# Rollback
address.delete()
- def test_serach_fields_for_supplier(self):
- from erpnext.controllers.queries import supplier_query
-
- frappe.db.set_single_value("Buying Settings", "supp_master_name", "Naming Series")
-
- supplier_name = create_supplier(supplier_name="Test Supplier 1").name
-
- make_property_setter(
- "Supplier", None, "search_fields", "supplier_group", "Data", for_doctype="Doctype"
- )
-
- data = supplier_query(
- "Supplier", supplier_name, "name", 0, 20, filters={"name": supplier_name}, as_dict=True
- )
-
- self.assertEqual(data[0].name, supplier_name)
- self.assertEqual(data[0].supplier_group, "Services")
- self.assertTrue("supplier_type" not in data[0])
-
- make_property_setter(
- "Supplier",
- None,
- "search_fields",
- "supplier_group, supplier_type",
- "Data",
- for_doctype="Doctype",
- )
- data = supplier_query(
- "Supplier", supplier_name, "name", 0, 20, filters={"name": supplier_name}, as_dict=True
- )
-
- self.assertEqual(data[0].name, supplier_name)
- self.assertEqual(data[0].supplier_group, "Services")
- self.assertEqual(data[0].supplier_type, "Company")
- self.assertTrue("supplier_type" in data[0])
-
- frappe.db.set_single_value("Buying Settings", "supp_master_name", "Supplier Name")
-
def create_supplier(**args):
args = frappe._dict(args)
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 8839773..8bdee22 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -169,6 +169,13 @@
if not self.get("is_return") and not self.get("is_debit_note"):
self.validate_qty_is_not_zero()
+ if (
+ self.doctype in ["Sales Invoice", "Purchase Invoice"]
+ and self.get("is_return")
+ and self.get("update_stock")
+ ):
+ self.validate_zero_qty_for_return_invoices_with_stock()
+
if self.get("_action") and self._action != "update_after_submit":
self.set_missing_values(for_validate=True)
@@ -603,23 +610,31 @@
)
def validate_due_date(self):
- if self.get("is_pos"):
+ if self.get("is_pos") or self.doctype not in ["Sales Invoice", "Purchase Invoice"]:
return
from erpnext.accounts.party import validate_due_date
- if self.doctype == "Sales Invoice":
+ posting_date = (
+ self.posting_date if self.doctype == "Sales Invoice" else (self.bill_date or self.posting_date)
+ )
+
+ # skip due date validation for records via Data Import
+ if frappe.flags.in_import and getdate(self.due_date) < getdate(posting_date):
+ self.due_date = posting_date
+
+ elif self.doctype == "Sales Invoice":
if not self.due_date:
frappe.throw(_("Due Date is mandatory"))
validate_due_date(
- self.posting_date,
+ posting_date,
self.due_date,
self.payment_terms_template,
)
elif self.doctype == "Purchase Invoice":
validate_due_date(
- self.bill_date or self.posting_date,
+ posting_date,
self.due_date,
self.bill_date,
self.payment_terms_template,
@@ -1045,6 +1060,18 @@
else:
return flt(args.get(field, 0) / self.get("conversion_rate", 1))
+ def validate_zero_qty_for_return_invoices_with_stock(self):
+ rows = []
+ for item in self.items:
+ if not flt(item.qty):
+ rows.append(item)
+ if rows:
+ frappe.throw(
+ _(
+ "For Return Invoices with Stock effect, '0' qty Items are not allowed. Following rows are affected: {0}"
+ ).format(frappe.bold(comma_and(["#" + str(x.idx) for x in rows])))
+ )
+
def validate_qty_is_not_zero(self):
for item in self.items:
if self.doctype == "Purchase Receipt" and item.rejected_qty:
@@ -2709,14 +2736,20 @@
else:
q = q.where(journal_acc.debit_in_account_currency > 0)
+ reference_or_condition = []
+
if include_unallocated:
- q = q.where((journal_acc.reference_name.isnull()) | (journal_acc.reference_name == ""))
+ reference_or_condition.append(journal_acc.reference_name.isnull())
+ reference_or_condition.append(journal_acc.reference_name == "")
if order_list:
- q = q.where(
+ reference_or_condition.append(
(journal_acc.reference_type == order_doctype) & ((journal_acc.reference_name).isin(order_list))
)
+ if reference_or_condition:
+ q = q.where(Criterion.any(reference_or_condition))
+
q = q.orderby(journal_entry.posting_date)
journal_entries = q.run(as_dict=True)
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 8211857..c530727 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -513,6 +513,14 @@
(not cint(self.is_return) and self.docstatus == 1)
or (cint(self.is_return) and self.docstatus == 2)
):
+ serial_and_batch_bundle = d.get("serial_and_batch_bundle")
+ if self.is_internal_transfer() and self.is_return and self.docstatus == 2:
+ serial_and_batch_bundle = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_detail_no": d.name, "warehouse": d.from_warehouse},
+ "serial_and_batch_bundle",
+ )
+
from_warehouse_sle = self.get_sl_entries(
d,
{
@@ -521,19 +529,24 @@
"outgoing_rate": d.rate,
"recalculate_rate": 1,
"dependant_sle_voucher_detail_no": d.name,
+ "serial_and_batch_bundle": serial_and_batch_bundle,
},
)
sl_entries.append(from_warehouse_sle)
+ type_of_transaction = "Inward"
+ if self.docstatus == 2:
+ type_of_transaction = "Outward"
+
sle = self.get_sl_entries(
d,
{
"actual_qty": flt(pr_qty),
"serial_and_batch_bundle": (
d.serial_and_batch_bundle
- if not self.is_internal_transfer()
- else self.get_package_for_target_warehouse(d)
+ if not self.is_internal_transfer() or self.is_return
+ else self.get_package_for_target_warehouse(d, type_of_transaction=type_of_transaction)
),
},
)
@@ -570,7 +583,17 @@
or (cint(self.is_return) and self.docstatus == 1)
):
from_warehouse_sle = self.get_sl_entries(
- d, {"actual_qty": -1 * pr_qty, "warehouse": d.from_warehouse, "recalculate_rate": 1}
+ d,
+ {
+ "actual_qty": -1 * pr_qty,
+ "warehouse": d.from_warehouse,
+ "recalculate_rate": 1,
+ "serial_and_batch_bundle": (
+ self.get_package_for_target_warehouse(d, d.from_warehouse, "Inward")
+ if self.is_internal_transfer() and self.is_return
+ else None
+ ),
+ },
)
sl_entries.append(from_warehouse_sle)
@@ -597,13 +620,15 @@
via_landed_cost_voucher=via_landed_cost_voucher,
)
- def get_package_for_target_warehouse(self, item) -> str:
+ def get_package_for_target_warehouse(self, item, warehouse=None, type_of_transaction=None) -> str:
if not item.serial_and_batch_bundle:
return ""
+ if not warehouse:
+ warehouse = item.warehouse
+
return self.make_package_for_transfer(
- item.serial_and_batch_bundle,
- item.warehouse,
+ item.serial_and_batch_bundle, warehouse, type_of_transaction=type_of_transaction
)
def update_ordered_and_reserved_qty(self):
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index bb1ed35..0de75d4 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -85,79 +85,6 @@
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
)
- # searches for customer
-
-
-@frappe.whitelist()
-@frappe.validate_and_sanitize_search_inputs
-def customer_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
- doctype = "Customer"
- conditions = []
- cust_master_name = frappe.defaults.get_user_default("cust_master_name")
-
- fields = ["name"]
- if cust_master_name != "Customer Name":
- fields.append("customer_name")
-
- fields = get_fields(doctype, fields)
- searchfields = frappe.get_meta(doctype).get_search_fields()
- searchfields = " or ".join(field + " like %(txt)s" for field in searchfields)
-
- return frappe.db.sql(
- """select {fields} from `tabCustomer`
- where docstatus < 2
- and ({scond}) and disabled=0
- {fcond} {mcond}
- order by
- (case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
- (case when locate(%(_txt)s, customer_name) > 0 then locate(%(_txt)s, customer_name) else 99999 end),
- idx desc,
- name, customer_name
- limit %(page_len)s offset %(start)s""".format(
- **{
- "fields": ", ".join(fields),
- "scond": searchfields,
- "mcond": get_match_cond(doctype),
- "fcond": get_filters_cond(doctype, filters, conditions).replace("%", "%%"),
- }
- ),
- {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
- as_dict=as_dict,
- )
-
-
-# searches for supplier
-@frappe.whitelist()
-@frappe.validate_and_sanitize_search_inputs
-def supplier_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
- doctype = "Supplier"
- supp_master_name = frappe.defaults.get_user_default("supp_master_name")
-
- fields = ["name"]
- if supp_master_name != "Supplier Name":
- fields.append("supplier_name")
-
- fields = get_fields(doctype, fields)
-
- return frappe.db.sql(
- """select {field} from `tabSupplier`
- where docstatus < 2
- and ({key} like %(txt)s
- or supplier_name like %(txt)s) and disabled=0
- and (on_hold = 0 or (on_hold = 1 and CURRENT_DATE > release_date))
- {mcond}
- order by
- (case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
- (case when locate(%(_txt)s, supplier_name) > 0 then locate(%(_txt)s, supplier_name) else 99999 end),
- idx desc,
- name, supplier_name
- limit %(page_len)s offset %(start)s""".format(
- **{"field": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)}
- ),
- {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
- as_dict=as_dict,
- )
-
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 1ddcaa7..5594816 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -423,6 +423,15 @@
]:
type_of_transaction = "Outward"
+ warehouse = source_doc.warehouse if qty_field == "stock_qty" else source_doc.rejected_warehouse
+ if source_parent.doctype in [
+ "Sales Invoice",
+ "POS Invoice",
+ "Delivery Note",
+ ] and source_parent.get("is_internal_customer"):
+ type_of_transaction = "Outward"
+ warehouse = source_doc.target_warehouse
+
cls_obj = SerialBatchCreation(
{
"type_of_transaction": type_of_transaction,
@@ -432,7 +441,7 @@
"returned_serial_nos": returned_serial_nos,
"voucher_type": source_parent.doctype,
"do_not_submit": True,
- "warehouse": source_doc.warehouse,
+ "warehouse": warehouse,
"has_serial_no": item_details.has_serial_no,
"has_batch_no": item_details.has_batch_no,
}
@@ -575,11 +584,14 @@
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) and not target_doc.get("use_serial_batch_fields"):
+ if not target_doc.get("use_serial_batch_fields"):
+ for qty_field in ["stock_qty", "rejected_qty"]:
+ if not target_doc.get(qty_field):
+ continue
+
update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field)
- elif target_doc.get(qty_field) and target_doc.get("use_serial_batch_fields"):
- update_non_bundled_serial_nos(source_doc, target_doc, source_parent)
+ elif target_doc.get("use_serial_batch_fields"):
+ update_non_bundled_serial_nos(source_doc, target_doc, source_parent)
def update_non_bundled_serial_nos(source_doc, target_doc, source_parent):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 359d721..9d86cb2 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -439,8 +439,10 @@
# Get incoming rate based on original item cost based on valuation method
qty = flt(d.get("stock_qty") or d.get("actual_qty"))
- if not d.incoming_rate or (
- get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")
+ if (
+ not d.incoming_rate
+ or self.is_internal_transfer()
+ or (get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return"))
):
d.incoming_rate = get_incoming_rate(
{
@@ -455,6 +457,8 @@
"voucher_no": self.name,
"voucher_detail_no": d.name,
"allow_zero_valuation": d.get("allow_zero_valuation"),
+ "batch_no": d.batch_no,
+ "serial_no": d.serial_no,
},
raise_error_if_no_rate=False,
)
@@ -527,13 +531,26 @@
self.make_sl_entries(sl_entries)
def get_sle_for_source_warehouse(self, item_row):
+ serial_and_batch_bundle = item_row.serial_and_batch_bundle
+ if serial_and_batch_bundle and self.is_internal_transfer() and self.is_return:
+ if self.docstatus == 1:
+ serial_and_batch_bundle = self.make_package_for_transfer(
+ serial_and_batch_bundle, item_row.warehouse, type_of_transaction="Inward"
+ )
+ else:
+ serial_and_batch_bundle = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_detail_no": item_row.name, "warehouse": item_row.warehouse},
+ "serial_and_batch_bundle",
+ )
+
sle = self.get_sl_entries(
item_row,
{
"actual_qty": -1 * flt(item_row.qty),
"incoming_rate": item_row.incoming_rate,
"recalculate_rate": cint(self.is_return),
- "serial_and_batch_bundle": item_row.serial_and_batch_bundle,
+ "serial_and_batch_bundle": serial_and_batch_bundle,
},
)
if item_row.target_warehouse and not cint(self.is_return):
@@ -554,9 +571,15 @@
if item_row.warehouse:
sle.dependant_sle_voucher_detail_no = item_row.name
- if item_row.serial_and_batch_bundle:
+ if item_row.serial_and_batch_bundle and not cint(self.is_return):
+ type_of_transaction = "Inward"
+ if cint(self.is_return):
+ type_of_transaction = "Outward"
+
sle["serial_and_batch_bundle"] = self.make_package_for_transfer(
- item_row.serial_and_batch_bundle, item_row.target_warehouse
+ item_row.serial_and_batch_bundle,
+ item_row.target_warehouse,
+ type_of_transaction=type_of_transaction,
)
return sle
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index a3fbdda..15eeff5 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -236,6 +236,14 @@
qty = row.get("rejected_qty")
warehouse = row.get("rejected_warehouse")
+ if (
+ self.is_internal_transfer()
+ and self.doctype in ["Sales Invoice", "Delivery Note"]
+ and self.is_return
+ ):
+ warehouse = row.get("target_warehouse") or row.get("warehouse")
+ type_of_transaction = "Outward"
+
bundle_details.update(
{
"qty": qty,
@@ -579,7 +587,7 @@
bundle_doc.warehouse = warehouse
bundle_doc.type_of_transaction = type_of_transaction
bundle_doc.voucher_type = self.doctype
- bundle_doc.voucher_no = self.name
+ bundle_doc.voucher_no = "" if self.is_new() or self.docstatus == 2 else self.name
bundle_doc.is_cancelled = 0
for row in bundle_doc.entries:
@@ -595,6 +603,7 @@
bundle_doc.calculate_qty_and_amount()
bundle_doc.flags.ignore_permissions = True
+ bundle_doc.flags.ignore_validate = True
bundle_doc.save(ignore_permissions=True)
return bundle_doc.name
diff --git a/erpnext/controllers/tests/test_queries.py b/erpnext/controllers/tests/test_queries.py
index 3a3bc1c..c536d1c 100644
--- a/erpnext/controllers/tests/test_queries.py
+++ b/erpnext/controllers/tests/test_queries.py
@@ -31,18 +31,6 @@
self.assertGreaterEqual(len(query(txt="_Test Lead")), 4)
self.assertEqual(len(query(txt="_Test Lead 4")), 1)
- def test_customer_query(self):
- query = add_default_params(queries.customer_query, "Customer")
-
- self.assertGreaterEqual(len(query(txt="_Test Customer")), 7)
- self.assertGreaterEqual(len(query(txt="_Test Customer USD")), 1)
-
- def test_supplier_query(self):
- query = add_default_params(queries.supplier_query, "Supplier")
-
- self.assertGreaterEqual(len(query(txt="_Test Supplier")), 7)
- self.assertGreaterEqual(len(query(txt="_Test Supplier USD")), 1)
-
def test_item_query(self):
query = add_default_params(queries.item_query, "Item")
diff --git a/erpnext/crm/doctype/lead/lead.js b/erpnext/crm/doctype/lead/lead.js
index 0b6cdf2..609eab7 100644
--- a/erpnext/crm/doctype/lead/lead.js
+++ b/erpnext/crm/doctype/lead/lead.js
@@ -17,10 +17,6 @@
}
onload() {
- this.frm.set_query("customer", function (doc, cdt, cdn) {
- return { query: "erpnext.controllers.queries.customer_query" };
- });
-
this.frm.set_query("lead_owner", function (doc, cdt, cdn) {
return { query: "frappe.core.doctype.user.user.user_query" };
});
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 308e6ca..9b996fe 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -282,9 +282,6 @@
before_tests = "erpnext.setup.utils.before_tests"
-standard_queries = {
- "Customer": "erpnext.controllers.queries.customer_query",
-}
period_closing_doctypes = [
"Sales Invoice",
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 4cd0530..2debf91 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -28,28 +28,6 @@
class TestBOM(FrappeTestCase):
@timeout
- def test_bom_qty(self):
- from erpnext.stock.doctype.item.test_item import make_item
-
- # No error.
- bom = frappe.new_doc("BOM")
- item = make_item(properties={"is_stock_item": 1})
- bom.item = fg_item.item_code
- bom.quantity = 1
- bom.append(
- "items",
- {
- "item_code": bom_item.item_code,
- "qty": 0,
- "uom": bom_item.stock_uom,
- "stock_uom": bom_item.stock_uom,
- "rate": 100.0,
- },
- )
- bom.save()
- self.assertEqual(bom.items[0].qty, 0)
-
- @timeout
def test_get_items(self):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index 42f6943..70e803d 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -9,6 +9,8 @@
"Job Card": "Create Job Card",
};
+ frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"];
+
// Set query for warehouses
frm.set_query("wip_warehouse", function () {
return {
diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js
index 16ac8db..49e8d84 100644
--- a/erpnext/projects/doctype/project/project.js
+++ b/erpnext/projects/doctype/project/project.js
@@ -27,8 +27,6 @@
};
};
- frm.set_query("customer", "erpnext.controllers.queries.customer_query");
-
frm.set_query("user", "users", function () {
return {
query: "erpnext.projects.doctype.project.project.get_users_for_project",
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index 1d0d47e..934becf 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -74,11 +74,6 @@
me.frm.set_query('billing_address', erpnext.queries.company_address_query);
erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype);
- if(this.frm.fields_dict.supplier) {
- this.frm.set_query("supplier", function() {
- return{ query: "erpnext.controllers.queries.supplier_query" }});
- }
-
this.frm.set_query("item_code", "items", function() {
if (me.frm.doc.is_subcontracted) {
var filters = {'supplier': me.frm.doc.supplier};
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index 3099184..4a525b2 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -26,7 +26,7 @@
item.discount_amount = flt(item.rate_with_margin) * flt(item.discount_percentage) / 100;
}
- if (item.discount_amount) {
+ if (item.discount_amount > 0) {
item_rate = flt((item.rate_with_margin) - (item.discount_amount), precision('rate', item));
item.discount_percentage = 100 * flt(item.discount_amount) / flt(item.rate_with_margin);
}
diff --git a/erpnext/public/js/queries.js b/erpnext/public/js/queries.js
index b7e685c..d7edac3 100644
--- a/erpnext/public/js/queries.js
+++ b/erpnext/public/js/queries.js
@@ -12,14 +12,6 @@
return { query: "erpnext.controllers.queries.lead_query" };
},
- customer: function () {
- return { query: "erpnext.controllers.queries.customer_query" };
- },
-
- supplier: function () {
- return { query: "erpnext.controllers.queries.supplier_query" };
- },
-
item: function (filters) {
var args = { query: "erpnext.controllers.queries.item_query" };
if (filters) args["filters"] = filters;
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index 24133b8..42d37bf4 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -542,6 +542,10 @@
frappe.throw(__("Please add atleast one Serial No / Batch No"));
}
+ if (!warehouse) {
+ frappe.throw(__("Please select a Warehouse"));
+ }
+
frappe
.call({
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.add_serial_batch_ledgers",
diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json
index db712d9..41c6311 100644
--- a/erpnext/selling/doctype/customer/customer.json
+++ b/erpnext/selling/doctype/customer/customer.json
@@ -583,7 +583,7 @@
"link_fieldname": "party"
}
],
- "modified": "2023-12-28 13:15:36.298369",
+ "modified": "2024-03-16 19:41:47.971815",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",
@@ -661,7 +661,7 @@
}
],
"quick_entry": 1,
- "search_fields": "customer_name,customer_group,territory, mobile_no,primary_address",
+ "search_fields": "customer_group,territory, mobile_no,primary_address",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py
index a8ebccd..7e6d6de 100644
--- a/erpnext/selling/doctype/customer/test_customer.py
+++ b/erpnext/selling/doctype/customer/test_customer.py
@@ -370,37 +370,6 @@
due_date = get_due_date("2017-01-22", "Customer", "_Test Customer")
self.assertEqual(due_date, "2017-01-22")
- def test_serach_fields_for_customer(self):
- from erpnext.controllers.queries import customer_query
-
- frappe.db.set_single_value("Selling Settings", "cust_master_name", "Naming Series")
-
- make_property_setter(
- "Customer", None, "search_fields", "customer_group", "Data", for_doctype="Doctype"
- )
-
- data = customer_query(
- "Customer", "_Test Customer", "", 0, 20, filters={"name": "_Test Customer"}, as_dict=True
- )
-
- self.assertEqual(data[0].name, "_Test Customer")
- self.assertEqual(data[0].customer_group, "_Test Customer Group")
- self.assertTrue("territory" not in data[0])
-
- make_property_setter(
- "Customer", None, "search_fields", "customer_group, territory", "Data", for_doctype="Doctype"
- )
- data = customer_query(
- "Customer", "_Test Customer", "", 0, 20, filters={"name": "_Test Customer"}, as_dict=True
- )
-
- self.assertEqual(data[0].name, "_Test Customer")
- self.assertEqual(data[0].customer_group, "_Test Customer Group")
- self.assertEqual(data[0].territory, "_Test Territory")
- self.assertTrue("territory" in data[0])
-
- frappe.db.set_single_value("Selling Settings", "cust_master_name", "Customer Name")
-
def test_parse_full_name(self):
first, middle, last = parse_full_name("John")
self.assertEqual(first, "John")
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 9fcda0d..7327fde 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -2141,6 +2141,40 @@
dn.submit()
dn.reload()
+ def test_auto_update_price_list(self):
+ item = make_item(
+ "_Test Auto Update Price List Item",
+ )
+
+ frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 1)
+ so = make_sales_order(
+ item_code=item.name, currency="USD", qty=1, rate=100, price_list_rate=100, do_not_submit=True
+ )
+ so.save()
+
+ item_price = frappe.db.get_value("Item Price", {"item_code": item.name}, "price_list_rate")
+ self.assertEqual(item_price, 100)
+
+ so = make_sales_order(
+ item_code=item.name, currency="USD", qty=1, rate=200, price_list_rate=100, do_not_submit=True
+ )
+ so.save()
+
+ item_price = frappe.db.get_value("Item Price", {"item_code": item.name}, "price_list_rate")
+ self.assertEqual(item_price, 100)
+
+ frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 1)
+ so = make_sales_order(
+ item_code=item.name, currency="USD", qty=1, rate=200, price_list_rate=200, do_not_submit=True
+ )
+ so.save()
+
+ item_price = frappe.db.get_value("Item Price", {"item_code": item.name}, "price_list_rate")
+ self.assertEqual(item_price, 200)
+
+ frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 0)
+ frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
+
def automatically_fetch_payment_terms(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings")
@@ -2206,13 +2240,14 @@
return so
-def create_dn_against_so(so, delivered_qty=0):
+def create_dn_against_so(so, delivered_qty=0, do_not_submit=False):
frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1)
dn = make_delivery_note(so)
dn.get("items")[0].qty = delivered_qty or 5
dn.insert()
- dn.submit()
+ if not do_not_submit:
+ dn.submit()
return dn
diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js
index d95ef58..fbee9c1 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_cart.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -295,10 +295,10 @@
<div class="customer-field"></div>
`);
const me = this;
- const query = { query: "erpnext.controllers.queries.customer_query" };
const allowed_customer_group = this.allowed_customer_groups || [];
+ let filters = {};
if (allowed_customer_group.length) {
- query.filters = {
+ filters = {
customer_group: ["in", allowed_customer_group],
};
}
@@ -308,7 +308,11 @@
fieldtype: "Link",
options: "Customer",
placeholder: __("Search by customer name, phone, email."),
- get_query: () => query,
+ get_query: function () {
+ return {
+ filters: filters,
+ };
+ },
onchange: function () {
if (this.value) {
const frm = me.events.get_frm();
diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py
index f2f1e4c..42bdf57 100644
--- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py
+++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py
@@ -197,6 +197,8 @@
):
details[p_key] += r.get(qty_or_amount_field, 0)
details[variance_key] = details.get(p_key) - details.get(target_key)
+ else:
+ details[variance_key] = details.get(p_key) - details.get(target_key)
details["total_achieved"] += details.get(p_key)
details["total_variance"] = details.get("total_achieved") - details.get("total_target")
@@ -209,31 +211,32 @@
parent_doc = frappe.qb.DocType(filters.get("doctype"))
child_doc = frappe.qb.DocType(filters.get("doctype") + " Item")
- sales_team = frappe.qb.DocType("Sales Team")
- query = (
- frappe.qb.from_(parent_doc)
- .inner_join(child_doc)
- .on(child_doc.parent == parent_doc.name)
- .inner_join(sales_team)
- .on(sales_team.parent == parent_doc.name)
- .select(
- child_doc.item_group,
- (child_doc.stock_qty * sales_team.allocated_percentage / 100).as_("stock_qty"),
- (child_doc.base_net_amount * sales_team.allocated_percentage / 100).as_("base_net_amount"),
- sales_team.sales_person,
- parent_doc[date_field],
- )
- .where(
- (parent_doc.docstatus == 1)
- & (parent_doc[date_field].between(fiscal_year.year_start_date, fiscal_year.year_end_date))
- )
- )
+ query = frappe.qb.from_(parent_doc).inner_join(child_doc).on(child_doc.parent == parent_doc.name)
if sales_field == "sales_person":
- query = query.where(sales_team.sales_person.isin(sales_users_or_territory_data))
+ sales_team = frappe.qb.DocType("Sales Team")
+ stock_qty = child_doc.stock_qty * sales_team.allocated_percentage / 100
+ net_amount = child_doc.base_net_amount * sales_team.allocated_percentage / 100
+ sales_field_col = sales_team[sales_field]
+
+ query = query.inner_join(sales_team).on(sales_team.parent == parent_doc.name)
else:
- query = query.where(parent_doc[sales_field].isin(sales_users_or_territory_data))
+ stock_qty = child_doc.stock_qty
+ net_amount = child_doc.base_net_amount
+ sales_field_col = parent_doc[sales_field]
+
+ query = query.select(
+ child_doc.item_group,
+ parent_doc[date_field],
+ (stock_qty).as_("stock_qty"),
+ (net_amount).as_("base_net_amount"),
+ sales_field_col,
+ ).where(
+ (parent_doc.docstatus == 1)
+ & (parent_doc[date_field].between(fiscal_year.year_start_date, fiscal_year.year_end_date))
+ & (sales_field_col.isin(sales_users_or_territory_data))
+ )
return query.run(as_dict=True)
diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/test_sales_partner_target_variance_based_on_item_group.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/test_sales_partner_target_variance_based_on_item_group.py
new file mode 100644
index 0000000..1718668
--- /dev/null
+++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/test_sales_partner_target_variance_based_on_item_group.py
@@ -0,0 +1,57 @@
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import flt, nowdate
+
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.accounts.utils import get_fiscal_year
+from erpnext.selling.report.sales_partner_target_variance_based_on_item_group.sales_partner_target_variance_based_on_item_group import (
+ execute,
+)
+from erpnext.selling.report.sales_person_target_variance_based_on_item_group.test_sales_person_target_variance_based_on_item_group import (
+ create_sales_target_doc,
+ create_target_distribution,
+)
+
+
+class TestSalesPartnerTargetVarianceBasedOnItemGroup(FrappeTestCase):
+ def setUp(self):
+ self.fiscal_year = get_fiscal_year(nowdate())[0]
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+ def test_achieved_target_and_variance_for_partner(self):
+ # Create a Target Distribution
+ distribution = create_target_distribution(self.fiscal_year)
+
+ # Create Sales Partner with targets for the current fiscal year
+ sales_partner = create_sales_target_doc(
+ "Sales Partner", "partner_name", "Sales Partner 1", self.fiscal_year, distribution.name
+ )
+
+ # Create a Sales Invoice for the Partner
+ si = create_sales_invoice(
+ rate=1000,
+ qty=20,
+ do_not_submit=True,
+ )
+ si.sales_partner = sales_partner
+ si.commission_rate = 5
+ si.submit()
+
+ # Check Achieved Target and Variance for the Sales Partner
+ result = execute(
+ frappe._dict(
+ {
+ "fiscal_year": self.fiscal_year,
+ "doctype": "Sales Invoice",
+ "period": "Yearly",
+ "target_on": "Quantity",
+ }
+ )
+ )[1]
+ row = frappe._dict(result[0])
+ self.assertSequenceEqual(
+ [flt(value, 2) for value in (row.total_target, row.total_achieved, row.total_variance)],
+ [50, 20, -30],
+ )
diff --git a/erpnext/selling/report/sales_person_target_variance_based_on_item_group/test_sales_person_target_variance_based_on_item_group.py b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/test_sales_person_target_variance_based_on_item_group.py
index 4ae5d2b..73ae6d0 100644
--- a/erpnext/selling/report/sales_person_target_variance_based_on_item_group/test_sales_person_target_variance_based_on_item_group.py
+++ b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/test_sales_person_target_variance_based_on_item_group.py
@@ -18,17 +18,17 @@
def test_achieved_target_and_variance(self):
# Create a Target Distribution
- distribution = frappe.new_doc("Monthly Distribution")
- distribution.distribution_id = "Target Report Distribution"
- distribution.fiscal_year = self.fiscal_year
- distribution.get_months()
- distribution.insert()
+ distribution = create_target_distribution(self.fiscal_year)
- # Create sales people with targets
- person_1 = create_sales_person_with_target("Sales Person 1", self.fiscal_year, distribution.name)
- person_2 = create_sales_person_with_target("Sales Person 2", self.fiscal_year, distribution.name)
+ # Create sales people with targets for the current fiscal year
+ person_1 = create_sales_target_doc(
+ "Sales Person", "sales_person_name", "Sales Person 1", self.fiscal_year, distribution.name
+ )
+ person_2 = create_sales_target_doc(
+ "Sales Person", "sales_person_name", "Sales Person 2", self.fiscal_year, distribution.name
+ )
- # Create a Sales Order with 50-50 contribution
+ # Create a Sales Order with 50-50 contribution between both Sales people
so = make_sales_order(
rate=1000,
qty=20,
@@ -69,10 +69,20 @@
)
-def create_sales_person_with_target(sales_person_name, fiscal_year, distribution_id):
- sales_person = frappe.new_doc("Sales Person")
- sales_person.sales_person_name = sales_person_name
- sales_person.append(
+def create_target_distribution(fiscal_year):
+ distribution = frappe.new_doc("Monthly Distribution")
+ distribution.distribution_id = "Target Report Distribution"
+ distribution.fiscal_year = fiscal_year
+ distribution.get_months()
+ return distribution.insert()
+
+
+def create_sales_target_doc(
+ sales_field_dt, sales_field_name, sales_field_value, fiscal_year, distribution_id
+):
+ sales_target_doc = frappe.new_doc(sales_field_dt)
+ sales_target_doc.set(sales_field_name, sales_field_value)
+ sales_target_doc.append(
"targets",
{
"fiscal_year": fiscal_year,
@@ -81,4 +91,6 @@
"distribution_id": distribution_id,
},
)
- return sales_person.insert()
+ if sales_field_dt == "Sales Partner":
+ sales_target_doc.commission_rate = 5
+ return sales_target_doc.insert()
diff --git a/erpnext/setup/demo_data/item.json b/erpnext/setup/demo_data/item.json
index 330e114..1702434 100644
--- a/erpnext/setup/demo_data/item.json
+++ b/erpnext/setup/demo_data/item.json
@@ -4,6 +4,7 @@
"item_group": "Demo Item Group",
"item_code": "SKU001",
"item_name": "T-shirt",
+ "valuation_rate": 400.0,
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/1484808/pexels-photo-1484808.jpeg"
},
@@ -11,6 +12,7 @@
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU002",
+ "valuation_rate": 300.0,
"item_name": "Laptop",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/3999538/pexels-photo-3999538.jpeg"
@@ -19,6 +21,7 @@
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU003",
+ "valuation_rate": 523.0,
"item_name": "Book",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/2422178/pexels-photo-2422178.jpeg"
@@ -27,6 +30,7 @@
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU004",
+ "valuation_rate": 725.0,
"item_name": "Smartphone",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/1647976/pexels-photo-1647976.jpeg"
@@ -35,6 +39,7 @@
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU005",
+ "valuation_rate": 222.0,
"item_name": "Sneakers",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/1598505/pexels-photo-1598505.jpeg"
@@ -43,6 +48,7 @@
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU006",
+ "valuation_rate": 420.0,
"item_name": "Coffee Mug",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/585753/pexels-photo-585753.jpeg"
@@ -51,6 +57,7 @@
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU007",
+ "valuation_rate": 375.0,
"item_name": "Television",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/8059376/pexels-photo-8059376.jpeg"
@@ -59,6 +66,7 @@
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU008",
+ "valuation_rate": 333.0,
"item_name": "Backpack",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/3731256/pexels-photo-3731256.jpeg"
@@ -67,6 +75,7 @@
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU009",
+ "valuation_rate": 700.0,
"item_name": "Headphones",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/3587478/pexels-photo-3587478.jpeg"
@@ -75,6 +84,7 @@
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU010",
+ "valuation_rate": 500.0,
"item_name": "Camera",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/51383/photo-camera-subject-photographer-51383.jpeg"
diff --git a/erpnext/setup/doctype/sales_partner/sales_partner.py b/erpnext/setup/doctype/sales_partner/sales_partner.py
index 1047360..a8a965d 100644
--- a/erpnext/setup/doctype/sales_partner/sales_partner.py
+++ b/erpnext/setup/doctype/sales_partner/sales_partner.py
@@ -54,25 +54,30 @@
self.partner_website = "http://" + self.partner_website
def get_context(self, context):
- address = frappe.db.get_value(
- "Address", {"sales_partner": self.name, "is_primary_address": 1}, "*", as_dict=True
+ address_names = frappe.db.get_all(
+ "Dynamic Link",
+ filters={"link_doctype": "Sales Partner", "link_name": self.name, "parenttype": "Address"},
+ pluck=["parent"],
)
- if address:
- city_state = ", ".join(filter(None, [address.city, address.state]))
- address_rows = [
- address.address_line1,
- address.address_line2,
- city_state,
- address.pincode,
- address.country,
- ]
- context.update(
+ addresses = []
+ for address_name in address_names:
+ address_doc = frappe.get_doc("Address", address_name)
+ city_state = ", ".join([item for item in [address_doc.city, address_doc.state] if item])
+ address_rows = [
+ address_doc.address_line1,
+ address_doc.address_line2,
+ city_state,
+ address_doc.pincode,
+ address_doc.country,
+ ]
+ addresses.append(
{
- "email": address.email_id,
+ "email": address_doc.email_id,
"partner_address": filter_strip_join(address_rows, "\n<br>"),
- "phone": filter_strip_join(cstr(address.phone).split(","), "\n<br>"),
+ "phone": filter_strip_join(cstr(address_doc.phone).split(","), "\n<br>"),
}
)
+ context["addresses"] = addresses
return context
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index a3903a3..2f52f21 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -251,6 +251,7 @@
def validate(self):
self.validate_posting_time()
super(DeliveryNote, self).validate()
+ self.validate_references()
self.set_status()
self.so_required()
self.validate_proj_cust()
@@ -333,6 +334,7 @@
"type_of_transaction": "Outward",
"serial_and_batch_bundle": bundle_id,
"item_code": item.get("item_code"),
+ "warehouse": item.get("warehouse"),
}
)
@@ -340,6 +342,58 @@
item.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle
+ def validate_references(self):
+ self.validate_sales_order_references()
+ self.validate_sales_invoice_references()
+
+ def validate_sales_order_references(self):
+ err_msg = ""
+ for item in self.items:
+ if (item.against_sales_order and not item.so_detail) or (
+ not item.against_sales_order and item.so_detail
+ ):
+ if not item.against_sales_order:
+ err_msg += (
+ _("'Sales Order' reference ({1}) is missing in row {0}").format(
+ frappe.bold(item.idx), frappe.bold("against_sales_order")
+ )
+ + "<br>"
+ )
+ else:
+ err_msg += (
+ _("'Sales Order Item' reference ({1}) is missing in row {0}").format(
+ frappe.bold(item.idx), frappe.bold("so_detail")
+ )
+ + "<br>"
+ )
+
+ if err_msg:
+ frappe.throw(err_msg, title=_("References to Sales Orders are Incomplete"))
+
+ def validate_sales_invoice_references(self):
+ err_msg = ""
+ for item in self.items:
+ if (item.against_sales_invoice and not item.si_detail) or (
+ not item.against_sales_invoice and item.si_detail
+ ):
+ if not item.against_sales_invoice:
+ err_msg += (
+ _("'Sales Invoice' reference ({1}) is missing in row {0}").format(
+ frappe.bold(item.idx), frappe.bold("against_sales_invoice")
+ )
+ + "<br>"
+ )
+ else:
+ err_msg += (
+ _("'Sales Invoice Item' reference ({1}) is missing in row {0}").format(
+ frappe.bold(item.idx), frappe.bold("si_detail")
+ )
+ + "<br>"
+ )
+
+ if err_msg:
+ frappe.throw(err_msg, title=_("References to Sales Invoices are Incomplete"))
+
def validate_proj_cust(self):
"""check for does customer belong to same project as entered.."""
if self.project and self.customer:
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 293ef9f..905287d 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -824,6 +824,15 @@
dn.cancel()
self.assertEqual(dn.status, "Cancelled")
+ def test_sales_order_reference_validation(self):
+ so = make_sales_order(po_no="12345")
+ dn = create_dn_against_so(so.name, delivered_qty=2, do_not_submit=True)
+ dn.items[0].against_sales_order = None
+ self.assertRaises(frappe.ValidationError, dn.save)
+ dn.reload()
+ dn.items[0].so_detail = None
+ self.assertRaises(frappe.ValidationError, dn.save)
+
def test_dn_billing_status_case1(self):
# SO -> DN -> SI
so = make_sales_order(po_no="12345")
@@ -1099,9 +1108,30 @@
dn.load_from_db()
batch_no = get_batch_from_bundle(dn.packed_items[0].serial_and_batch_bundle)
+ packed_name = dn.packed_items[0].name
self.assertTrue(batch_no)
+ dn.cancel()
+
+ # Cancel the reposting entry
+ reposting_entries = frappe.get_all("Repost Item Valuation", filters={"docstatus": 1})
+ for entry in reposting_entries:
+ doc = frappe.get_doc("Repost Item Valuation", entry.name)
+ doc.cancel()
+ doc.delete()
+
+ frappe.db.set_single_value("Accounts Settings", "delete_linked_ledger_entries", 1)
+
+ dn.reload()
+ dn.delete()
+
+ bundle = frappe.db.get_value(
+ "Serial and Batch Bundle", {"voucher_detail_no": packed_name}, "name"
+ )
+ self.assertFalse(bundle)
+
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
+ frappe.db.set_single_value("Accounts Settings", "delete_linked_ledger_entries", 0)
def test_payment_terms_are_fetched_when_creating_sales_invoice(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 7a38024..5310a0f 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -406,14 +406,6 @@
};
};
- frm.fields_dict.customer_items.grid.get_field("customer_name").get_query = function (doc, cdt, cdn) {
- return { query: "erpnext.controllers.queries.customer_query" };
- };
-
- frm.fields_dict.supplier_items.grid.get_field("supplier").get_query = function (doc, cdt, cdn) {
- return { query: "erpnext.controllers.queries.supplier_query" };
- };
-
frm.fields_dict["item_defaults"].grid.get_field("default_warehouse").get_query = function (
doc,
cdt,
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 8a1f79d..627520c 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -184,7 +184,11 @@
def delink_serial_and_batch_bundle(self):
for row in self.locations:
- if row.serial_and_batch_bundle:
+ if (
+ row.serial_and_batch_bundle
+ and frappe.db.get_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "docstatus")
+ == 1
+ ):
frappe.db.set_value(
"Serial and Batch Bundle",
row.serial_and_batch_bundle,
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index fa2c21f..5cf2080 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -2559,6 +2559,280 @@
self.assertEqual(row.serial_no, "\n".join(serial_nos[:2]))
self.assertEqual(row.rejected_serial_no, serial_nos[2])
+ def test_internal_transfer_with_serial_batch_items_and_their_valuation(self):
+ from erpnext.controllers.sales_and_purchase_return import make_return_doc
+ from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
+ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+
+ prepare_data_for_internal_transfer()
+
+ customer = "_Test Internal Customer 2"
+ company = "_Test Company with perpetual inventory"
+
+ batch_item_doc = make_item(
+ "_Test Batch Item For Stock Transfer",
+ {"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "BT-BIFST-.####"},
+ )
+
+ serial_item_doc = make_item(
+ "_Test Serial No Item For Stock Transfer",
+ {"has_serial_no": 1, "serial_no_series": "BT-BIFST-.####"},
+ )
+
+ inward_entry = make_purchase_receipt(
+ item_code=batch_item_doc.name,
+ qty=10,
+ rate=150,
+ warehouse="Stores - TCP1",
+ company="_Test Company with perpetual inventory",
+ use_serial_batch_fields=1,
+ do_not_submit=1,
+ )
+
+ inward_entry.append(
+ "items",
+ {
+ "item_code": serial_item_doc.name,
+ "qty": 15,
+ "rate": 250,
+ "item_name": serial_item_doc.item_name,
+ "conversion_factor": 1.0,
+ "uom": serial_item_doc.stock_uom,
+ "stock_uom": serial_item_doc.stock_uom,
+ "warehouse": "Stores - TCP1",
+ "use_serial_batch_fields": 1,
+ },
+ )
+
+ inward_entry.submit()
+ inward_entry.reload()
+
+ for row in inward_entry.items:
+ self.assertTrue(row.serial_and_batch_bundle)
+
+ inter_transfer_dn = create_delivery_note(
+ item_code=inward_entry.items[0].item_code,
+ company=company,
+ customer=customer,
+ cost_center="Main - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ qty=10,
+ rate=500,
+ warehouse="Stores - TCP1",
+ target_warehouse="Work In Progress - TCP1",
+ batch_no=get_batch_from_bundle(inward_entry.items[0].serial_and_batch_bundle),
+ use_serial_batch_fields=1,
+ do_not_submit=1,
+ )
+
+ inter_transfer_dn.append(
+ "items",
+ {
+ "item_code": serial_item_doc.name,
+ "qty": 15,
+ "rate": 350,
+ "item_name": serial_item_doc.item_name,
+ "conversion_factor": 1.0,
+ "uom": serial_item_doc.stock_uom,
+ "stock_uom": serial_item_doc.stock_uom,
+ "warehouse": "Stores - TCP1",
+ "target_warehouse": "Work In Progress - TCP1",
+ "serial_no": "\n".join(
+ get_serial_nos_from_bundle(inward_entry.items[1].serial_and_batch_bundle)
+ ),
+ "use_serial_batch_fields": 1,
+ },
+ )
+
+ inter_transfer_dn.submit()
+ inter_transfer_dn.reload()
+ for row in inter_transfer_dn.items:
+ if row.item_code == batch_item_doc.name:
+ self.assertEqual(row.rate, 150.0)
+ else:
+ self.assertEqual(row.rate, 250.0)
+
+ self.assertTrue(row.serial_and_batch_bundle)
+
+ inter_transfer_pr = make_inter_company_purchase_receipt(inter_transfer_dn.name)
+ for row in inter_transfer_pr.items:
+ row.from_warehouse = "Work In Progress - TCP1"
+ row.warehouse = "Stores - TCP1"
+ inter_transfer_pr.submit()
+
+ for row in inter_transfer_pr.items:
+ if row.item_code == batch_item_doc.name:
+ self.assertEqual(row.rate, 150.0)
+ else:
+ self.assertEqual(row.rate, 250.0)
+
+ self.assertTrue(row.serial_and_batch_bundle)
+
+ inter_transfer_pr_return = make_return_doc("Purchase Receipt", inter_transfer_pr.name)
+
+ inter_transfer_pr_return.submit()
+ inter_transfer_pr_return.reload()
+ for row in inter_transfer_pr_return.items:
+ self.assertTrue(row.serial_and_batch_bundle)
+ if row.item_code == serial_item_doc.name:
+ self.assertEqual(row.rate, 250.0)
+ serial_nos = get_serial_nos_from_bundle(row.serial_and_batch_bundle)
+ for sn in serial_nos:
+ serial_no_details = frappe.db.get_value("Serial No", sn, ["status", "warehouse"], as_dict=1)
+ self.assertTrue(serial_no_details.status == "Active")
+ self.assertEqual(serial_no_details.warehouse, "Work In Progress - TCP1")
+
+ inter_transfer_dn_return = make_return_doc("Delivery Note", inter_transfer_dn.name)
+ inter_transfer_dn_return.posting_date = today()
+ inter_transfer_dn_return.posting_time = nowtime()
+ for row in inter_transfer_dn_return.items:
+ row.target_warehouse = "Work In Progress - TCP1"
+
+ inter_transfer_dn_return.submit()
+ inter_transfer_dn_return.reload()
+
+ for row in inter_transfer_dn_return.items:
+ self.assertTrue(row.serial_and_batch_bundle)
+
+ def test_internal_transfer_with_serial_batch_items_without_user_serial_batch_fields(self):
+ from erpnext.controllers.sales_and_purchase_return import make_return_doc
+ from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
+ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+
+ frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
+
+ prepare_data_for_internal_transfer()
+
+ customer = "_Test Internal Customer 2"
+ company = "_Test Company with perpetual inventory"
+
+ batch_item_doc = make_item(
+ "_Test Batch Item For Stock Transfer USE SERIAL BATCH FIELDS",
+ {"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "USBF-BT-BIFST-.####"},
+ )
+
+ serial_item_doc = make_item(
+ "_Test Serial No Item For Stock Transfer USE SERIAL BATCH FIELDS",
+ {"has_serial_no": 1, "serial_no_series": "USBF-BT-BIFST-.####"},
+ )
+
+ inward_entry = make_purchase_receipt(
+ item_code=batch_item_doc.name,
+ qty=10,
+ rate=150,
+ warehouse="Stores - TCP1",
+ company="_Test Company with perpetual inventory",
+ use_serial_batch_fields=0,
+ do_not_submit=1,
+ )
+
+ inward_entry.append(
+ "items",
+ {
+ "item_code": serial_item_doc.name,
+ "qty": 15,
+ "rate": 250,
+ "item_name": serial_item_doc.item_name,
+ "conversion_factor": 1.0,
+ "uom": serial_item_doc.stock_uom,
+ "stock_uom": serial_item_doc.stock_uom,
+ "warehouse": "Stores - TCP1",
+ "use_serial_batch_fields": 0,
+ },
+ )
+
+ inward_entry.submit()
+ inward_entry.reload()
+
+ for row in inward_entry.items:
+ self.assertTrue(row.serial_and_batch_bundle)
+
+ inter_transfer_dn = create_delivery_note(
+ item_code=inward_entry.items[0].item_code,
+ company=company,
+ customer=customer,
+ cost_center="Main - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ qty=10,
+ rate=500,
+ warehouse="Stores - TCP1",
+ target_warehouse="Work In Progress - TCP1",
+ batch_no=get_batch_from_bundle(inward_entry.items[0].serial_and_batch_bundle),
+ use_serial_batch_fields=0,
+ do_not_submit=1,
+ )
+
+ inter_transfer_dn.append(
+ "items",
+ {
+ "item_code": serial_item_doc.name,
+ "qty": 15,
+ "rate": 350,
+ "item_name": serial_item_doc.item_name,
+ "conversion_factor": 1.0,
+ "uom": serial_item_doc.stock_uom,
+ "stock_uom": serial_item_doc.stock_uom,
+ "warehouse": "Stores - TCP1",
+ "target_warehouse": "Work In Progress - TCP1",
+ "serial_no": "\n".join(
+ get_serial_nos_from_bundle(inward_entry.items[1].serial_and_batch_bundle)
+ ),
+ "use_serial_batch_fields": 0,
+ },
+ )
+
+ inter_transfer_dn.submit()
+ inter_transfer_dn.reload()
+ for row in inter_transfer_dn.items:
+ if row.item_code == batch_item_doc.name:
+ self.assertEqual(row.rate, 150.0)
+ else:
+ self.assertEqual(row.rate, 250.0)
+
+ self.assertTrue(row.serial_and_batch_bundle)
+
+ inter_transfer_pr = make_inter_company_purchase_receipt(inter_transfer_dn.name)
+ for row in inter_transfer_pr.items:
+ row.from_warehouse = "Work In Progress - TCP1"
+ row.warehouse = "Stores - TCP1"
+ inter_transfer_pr.submit()
+
+ for row in inter_transfer_pr.items:
+ if row.item_code == batch_item_doc.name:
+ self.assertEqual(row.rate, 150.0)
+ else:
+ self.assertEqual(row.rate, 250.0)
+
+ self.assertTrue(row.serial_and_batch_bundle)
+
+ inter_transfer_pr_return = make_return_doc("Purchase Receipt", inter_transfer_pr.name)
+
+ inter_transfer_pr_return.submit()
+ inter_transfer_pr_return.reload()
+ for row in inter_transfer_pr_return.items:
+ self.assertTrue(row.serial_and_batch_bundle)
+ if row.item_code == serial_item_doc.name:
+ self.assertEqual(row.rate, 250.0)
+ serial_nos = get_serial_nos_from_bundle(row.serial_and_batch_bundle)
+ for sn in serial_nos:
+ serial_no_details = frappe.db.get_value("Serial No", sn, ["status", "warehouse"], as_dict=1)
+ self.assertTrue(serial_no_details.status == "Active")
+ self.assertEqual(serial_no_details.warehouse, "Work In Progress - TCP1")
+
+ inter_transfer_dn_return = make_return_doc("Delivery Note", inter_transfer_dn.name)
+ inter_transfer_dn_return.posting_date = today()
+ inter_transfer_dn_return.posting_time = nowtime()
+ for row in inter_transfer_dn_return.items:
+ row.target_warehouse = "Work In Progress - TCP1"
+
+ inter_transfer_dn_return.submit()
+ inter_transfer_dn_return.reload()
+
+ for row in inter_transfer_dn_return.items:
+ self.assertTrue(row.serial_and_batch_bundle)
+
+ frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
+
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json
index 7a58462..59ef43e 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json
@@ -113,6 +113,7 @@
{
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
+ "in_standard_filter": 1,
"label": "Voucher No",
"no_copy": 1,
"options": "voucher_type",
@@ -250,7 +251,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-12-07 17:56:55.528563",
+ "modified": "2024-03-15 15:22:24.003486",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial and Batch Bundle",
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
index 08cb3ca..58971e8 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
@@ -778,6 +778,10 @@
or_filters=or_filters,
)
+ if not vouchers and self.voucher_type == "Delivery Note":
+ frappe.db.set_value("Packed Item", self.voucher_detail_no, "serial_and_batch_bundle", None)
+ return
+
for voucher in vouchers:
if voucher.get("current_serial_and_batch_bundle"):
frappe.db.set_value(self.child_table, voucher.name, "current_serial_and_batch_bundle", None)
@@ -801,6 +805,7 @@
self.set_purchase_document_no()
def on_submit(self):
+ self.validate_batch_inventory()
self.validate_serial_nos_inventory()
def set_purchase_document_no(self):
@@ -827,6 +832,13 @@
if not self.has_batch_no:
return
+ if (
+ self.voucher_type == "Stock Reconciliation"
+ and self.type_of_transaction == "Outward"
+ and frappe.db.get_value("Stock Reconciliation Item", self.voucher_detail_no, "qty") > 0
+ ):
+ return
+
batches = [d.batch_no for d in self.entries if d.batch_no]
if not batches:
return
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index b206406..5b321de 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -2611,6 +2611,7 @@
"type_of_transaction": "Outward",
"serial_and_batch_bundle": item.get("serial_and_batch_bundle"),
"item_code": item.get("item_code"),
+ "warehouse": item.get("t_warehouse"),
}
)
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 39166e2..01a43b3 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -1008,6 +1008,7 @@
"type_of_transaction": "Inward",
"serial_and_batch_bundle": s2.items[0].serial_and_batch_bundle,
"item_code": "_Test Serialized Item",
+ "warehouse": "_Test Warehouse - _TC",
}
)
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 3356ad5..0311481 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -154,7 +154,6 @@
{
"current_serial_and_batch_bundle": sn_doc.name,
"current_serial_no": "",
- "batch_no": "",
}
)
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 3c8b808..12049f1 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -820,7 +820,9 @@
price_list_rate = get_price_list_rate_for(args, item_doc.variant_of)
# insert in database
- if price_list_rate is None:
+ if price_list_rate is None or frappe.db.get_single_value(
+ "Stock Settings", "update_existing_price_list_rate"
+ ):
if args.price_list and args.rate:
insert_item_price(args)
return out
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py
index 2e4b08c..e98351a 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.py
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.py
@@ -60,6 +60,7 @@
if filters.get("batch_no") or inventory_dimension_filters_applied:
actual_qty += flt(sle.actual_qty, precision)
stock_value += sle.stock_value_difference
+ batch_balance_dict[sle.batch_no] += sle.actual_qty
if sle.voucher_type == "Stock Reconciliation" and not sle.actual_qty:
actual_qty = sle.qty_after_transaction
diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
index 4e87fa0..7b42103 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -820,6 +820,10 @@
self.remove_returned_serial_nos(new_package)
new_package.docstatus = 0
+ new_package.warehouse = self.warehouse
+ new_package.voucher_no = ""
+ new_package.posting_date = today()
+ new_package.posting_time = nowtime()
new_package.type_of_transaction = self.type_of_transaction
new_package.returned_against = self.get("returned_against")
new_package.save()
diff --git a/erpnext/templates/generators/sales_partner.html b/erpnext/templates/generators/sales_partner.html
index 39138d3..9dd2db9 100644
--- a/erpnext/templates/generators/sales_partner.html
+++ b/erpnext/templates/generators/sales_partner.html
@@ -8,18 +8,20 @@
<div class="partner-content" itemscope itemtype="http://schema.org/Organization">
<div class="row">
<div class="col-md-4">
- {% if logo -%}
+ {% if logo %}
<img itemprop="brand" src="{{ logo }}" class="partner-logo"
alt="{{ partner_name }}" title="{{ partner_name }}" />
<br><br>
- {%- endif %}
- <address>
- {% if partner_website -%}<p><a href="{{ partner_website }}"
- target="_blank">{{ partner_website }}</a></p>{%- endif %}
- {% if partner_address -%}<p itemprop="address">{{ partner_address }}</p>{%- endif %}
- {% if phone -%}<p itemprop="telephone">{{ phone }}</p>{%- endif %}
- {% if email -%}<p itemprop="email"><span class="fa fa-envelope"></span> {{ email }}</p>{%- endif %}
- </address>
+ {% endif %}
+ {% if addresses %}
+ {% for address in addresses %}
+ <address>
+ {% if address.partner_address %}<p itemprop="address">{{ address.partner_address }}</p>{% endif %}
+ {% if address.phone %}<p itemprop="telephone">{{ address.phone }}</p>{% endif %}
+ {% if address.email %}<p itemprop="email"><span class="fa fa-envelope"></span> {{ address.email }}</p>{% endif %}
+ </address>
+ {% endfor %}
+ {% endif %}
</div>
<div class="col-md-8">
<p>{{ description }}</p>