diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index 0c71b41a7c..48337cee64 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -8,8 +8,9 @@ sudo apt update && sudo apt install redis-server libcups2-dev
pip install frappe-bench
+githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}
frappeuser=${FRAPPE_USER:-"frappe"}
-frappebranch=${FRAPPE_BRANCH:-${GITHUB_BASE_REF:-${GITHUB_REF##*/}}}
+frappebranch=${FRAPPE_BRANCH:-$githubbranch}
git clone "https://github.com/${frappeuser}/frappe" --branch "${frappebranch}" --depth 1
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
@@ -60,7 +61,7 @@ sed -i 's/schedule:/# schedule:/g' Procfile
sed -i 's/socketio:/# socketio:/g' Procfile
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
-bench get-app payments
+bench get-app payments --branch ${githubbranch%"-hotfix"}
bench get-app erpnext "${GITHUB_WORKSPACE}"
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py
index ec0ba081c8..0404d1c677 100644
--- a/erpnext/accounts/doctype/account/account.py
+++ b/erpnext/accounts/doctype/account/account.py
@@ -394,7 +394,13 @@ def update_account_number(name, account_name, account_number=None, from_descenda
if ancestors and not allow_independent_account_creation:
for ancestor in ancestors:
- if frappe.db.get_value("Account", {"account_name": old_acc_name, "company": ancestor}, "name"):
+ old_name = frappe.db.get_value(
+ "Account",
+ {"account_number": old_acc_number, "account_name": old_acc_name, "company": ancestor},
+ "name",
+ )
+
+ if old_name:
# same account in parent company exists
allow_child_account_creation = _("Allow Account Creation Against Child Company")
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index e3d9c26b2d..c9e3998ac8 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -221,12 +221,15 @@ class PaymentReconciliation(Document):
def get_difference_amount(self, payment_entry, invoice, allocated_amount):
difference_amount = 0
- if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
- "exchange_rate", 1
- ):
- allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount
- allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount
- difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
+ if frappe.get_cached_value(
+ "Account", self.receivable_payable_account, "account_currency"
+ ) != frappe.get_cached_value("Company", self.company, "default_currency"):
+ if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
+ "exchange_rate", 1
+ ):
+ allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount
+ allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount
+ difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
return difference_amount
diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
index f9dda0593b..3be11ae31a 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
@@ -5,7 +5,7 @@ import unittest
import frappe
from frappe import qb
-from frappe.tests.utils import FrappeTestCase
+from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, flt, nowdate
from erpnext import get_default_cost_center
@@ -349,6 +349,11 @@ class TestPaymentReconciliation(FrappeTestCase):
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}))
+
+ # Difference amount should not be calculated for base currency accounts
+ for row in pr.allocation:
+ self.assertEqual(flt(row.get("difference_amount")), 0.0)
+
pr.reconcile()
si.reload()
@@ -390,6 +395,11 @@ class TestPaymentReconciliation(FrappeTestCase):
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}))
+
+ # Difference amount should not be calculated for base currency accounts
+ for row in pr.allocation:
+ self.assertEqual(flt(row.get("difference_amount")), 0.0)
+
pr.reconcile()
# check PR tool output
@@ -414,6 +424,11 @@ class TestPaymentReconciliation(FrappeTestCase):
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}))
+
+ # Difference amount should not be calculated for base currency accounts
+ for row in pr.allocation:
+ self.assertEqual(flt(row.get("difference_amount")), 0.0)
+
pr.reconcile()
# assert outstanding
@@ -450,6 +465,11 @@ class TestPaymentReconciliation(FrappeTestCase):
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}))
+
+ # Difference amount should not be calculated for base currency accounts
+ for row in pr.allocation:
+ self.assertEqual(flt(row.get("difference_amount")), 0.0)
+
pr.reconcile()
self.assertEqual(pr.get("invoices"), [])
@@ -824,6 +844,52 @@ class TestPaymentReconciliation(FrappeTestCase):
payment_vouchers = [x.get("reference_name") for x in pr.get("payments")]
self.assertCountEqual(payment_vouchers, [je2.name, pe2.name])
+ @change_settings(
+ "Accounts Settings",
+ {
+ "allow_multi_currency_invoices_against_single_party_account": 1,
+ },
+ )
+ def test_no_difference_amount_for_base_currency_accounts(self):
+ # Make Sale Invoice
+ si = self.create_sales_invoice(
+ qty=1, rate=1, posting_date=nowdate(), do_not_save=True, do_not_submit=True
+ )
+ si.customer = self.customer
+ si.currency = "EUR"
+ si.conversion_rate = 85
+ si.debit_to = self.debit_to
+ si.save().submit()
+
+ # Make payment using Payment Entry
+ pe1 = create_payment_entry(
+ company=self.company,
+ payment_type="Receive",
+ party_type="Customer",
+ party=self.customer,
+ paid_from=self.debit_to,
+ paid_to=self.bank,
+ paid_amount=100,
+ )
+
+ pe1.save()
+ pe1.submit()
+
+ pr = self.create_payment_reconciliation()
+ pr.party = self.customer
+ pr.receivable_payable_account = self.debit_to
+ pr.get_unreconciled_entries()
+
+ self.assertEqual(len(pr.invoices), 1)
+ self.assertEqual(len(pr.payments), 1)
+
+ invoices = [x.as_dict() for x in pr.invoices]
+ payments = [pr.payments[0].as_dict()]
+ pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+
+ self.assertEqual(pr.allocation[0].allocated_amount, 85)
+ self.assertEqual(pr.allocation[0].difference_amount, 0)
+
def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name):
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 3920d4cf09..b9680dfb3b 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
@@ -15,7 +15,7 @@
{{ _("STATEMENTS OF ACCOUNTS") }}
-
{{ _("Customer: ") }} {{filters.party[0] }}
+ {{ _("Customer: ") }} {{filters.party_name[0] }}
{{ _("Date: ") }}
{{ frappe.format(filters.from_date, 'Date')}}
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
index a48c0272ff..a482931a8e 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
@@ -24,7 +24,7 @@ from erpnext.accounts.report.general_ledger.general_ledger import execute as get
class ProcessStatementOfAccounts(Document):
def validate(self):
if not self.subject:
- self.subject = "Statement Of Accounts for {{ customer.name }}"
+ self.subject = "Statement Of Accounts for {{ customer.customer_name }}"
if not self.body:
self.body = "Hello {{ customer.name }},
PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}."
@@ -87,6 +87,7 @@ def get_report_pdf(doc, consolidated=True):
"account": [doc.account] if doc.account else None,
"party_type": "Customer",
"party": [entry.customer],
+ "party_name": [entry.customer_name] if entry.customer_name else None,
"presentation_currency": presentation_currency,
"group_by": doc.group_by,
"currency": doc.currency,
@@ -156,7 +157,7 @@ def get_customers_based_on_territory_or_customer_group(customer_collection, coll
]
return frappe.get_list(
"Customer",
- fields=["name", "email_id"],
+ fields=["name", "customer_name", "email_id"],
filters=[[fields_dict[customer_collection], "IN", selected]],
)
@@ -179,7 +180,7 @@ def get_customers_based_on_sales_person(sales_person):
if sales_person_records.get("Customer"):
return frappe.get_list(
"Customer",
- fields=["name", "email_id"],
+ fields=["name", "customer_name", "email_id"],
filters=[["name", "in", list(sales_person_records["Customer"])]],
)
else:
@@ -228,7 +229,7 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
if customer_collection == "Sales Partner":
customers = frappe.get_list(
"Customer",
- fields=["name", "email_id"],
+ fields=["name", "customer_name", "email_id"],
filters=[["default_sales_partner", "=", collection_name]],
)
else:
@@ -245,7 +246,12 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
continue
customer_list.append(
- {"name": customer.name, "primary_email": primary_email, "billing_email": billing_email}
+ {
+ "name": customer.name,
+ "customer_name": customer.customer_name,
+ "primary_email": primary_email,
+ "billing_email": billing_email,
+ }
)
return customer_list
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json b/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json
index dd04dc1b3c..8bffd6a93b 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json
+++ b/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json
@@ -1,12 +1,12 @@
{
"actions": [],
- "allow_workflow": 1,
"creation": "2020-08-03 16:35:21.852178",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"customer",
+ "customer_name",
"billing_email",
"primary_email"
],
@@ -30,11 +30,18 @@
"fieldtype": "Read Only",
"in_list_view": 1,
"label": "Billing Email"
+ },
+ {
+ "fetch_from": "customer.customer_name",
+ "fieldname": "customer_name",
+ "fieldtype": "Data",
+ "label": "Customer Name",
+ "read_only": 1
}
],
"istable": 1,
"links": [],
- "modified": "2020-08-03 22:55:38.875601",
+ "modified": "2023-03-13 00:12:34.508086",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts Customer",
@@ -43,5 +50,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 2f4e45e618..2a8ff40413 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -32,9 +32,6 @@
"cost_center",
"dimension_col_break",
"project",
- "column_break_27",
- "campaign",
- "source",
"currency_and_price_list",
"currency",
"conversion_rate",
@@ -203,7 +200,9 @@
"more_information",
"status",
"inter_company_invoice_reference",
+ "campaign",
"represents_company",
+ "source",
"customer_group",
"col_break23",
"is_internal_customer",
@@ -2083,10 +2082,6 @@
"fieldname": "company_addr_col_break",
"fieldtype": "Column Break"
},
- {
- "fieldname": "column_break_27",
- "fieldtype": "Column Break"
- },
{
"fieldname": "column_break_52",
"fieldtype": "Column Break"
@@ -2143,11 +2138,10 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2023-01-28 19:45:47.538163",
+ "modified": "2023-03-13 11:43:15.883055",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
- "name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 0ffd9463e6..6051c9915d 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -266,16 +266,16 @@ class TestSalesInvoice(unittest.TestCase):
"_Test Account Education Cess - _TC": [3, 1618, 0.06, 32.36],
"_Test Account S&H Education Cess - _TC": [1.5, 1619.5, 0.03, 32.39],
"_Test Account CST - _TC": [32.5, 1652, 0.65, 33.04],
- "_Test Account VAT - _TC": [156.5, 1808.5, 3.13, 36.17],
- "_Test Account Discount - _TC": [-181.0, 1627.5, -3.62, 32.55],
+ "_Test Account VAT - _TC": [156.0, 1808.0, 3.12, 36.16],
+ "_Test Account Discount - _TC": [-181.0, 1627.0, -3.62, 32.54],
}
for d in si.get("taxes"):
for i, k in enumerate(expected_values["keys"]):
self.assertEqual(d.get(k), expected_values[d.account_head][i])
- self.assertEqual(si.base_grand_total, 1627.5)
- self.assertEqual(si.grand_total, 32.55)
+ self.assertEqual(si.base_grand_total, 1627.0)
+ self.assertEqual(si.grand_total, 32.54)
def test_sales_invoice_with_discount_and_inclusive_tax(self):
si = create_sales_invoice(qty=100, rate=50, do_not_save=True)
@@ -401,10 +401,10 @@ class TestSalesInvoice(unittest.TestCase):
"_Test Account S&H Education Cess - _TC": [1.4, 1.30, 1297.67],
"_Test Account CST - _TC": [27.88, 25.95, 1323.62],
"_Test Account VAT - _TC": [156.25, 145.43, 1469.05],
- "_Test Account Customs Duty - _TC": [125, 116.35, 1585.40],
- "_Test Account Shipping Charges - _TC": [100, 100, 1685.40],
- "_Test Account Discount - _TC": [-180.33, -168.54, 1516.86],
- "_Test Account Service Tax - _TC": [-18.03, -16.85, 1500.01],
+ "_Test Account Customs Duty - _TC": [125, 116.34, 1585.39],
+ "_Test Account Shipping Charges - _TC": [100, 100, 1685.39],
+ "_Test Account Discount - _TC": [-180.33, -168.54, 1516.85],
+ "_Test Account Service Tax - _TC": [-18.03, -16.85, 1500.00],
}
for d in si.get("taxes"):
@@ -413,7 +413,7 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(si.base_grand_total, 1500)
self.assertEqual(si.grand_total, 1500)
- self.assertEqual(si.rounding_adjustment, -0.01)
+ self.assertEqual(si.rounding_adjustment, 0.0)
def test_discount_amount_gl_entry(self):
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
@@ -454,7 +454,7 @@ class TestSalesInvoice(unittest.TestCase):
[test_records[3]["taxes"][2]["account_head"], 0.0, 1.30],
[test_records[3]["taxes"][3]["account_head"], 0.0, 25.95],
[test_records[3]["taxes"][4]["account_head"], 0.0, 145.43],
- [test_records[3]["taxes"][5]["account_head"], 0.0, 116.35],
+ [test_records[3]["taxes"][5]["account_head"], 0.0, 116.34],
[test_records[3]["taxes"][6]["account_head"], 0.0, 100],
[test_records[3]["taxes"][7]["account_head"], 168.54, 0.0],
["_Test Account Service Tax - _TC", 16.85, 0.0],
@@ -1614,7 +1614,7 @@ class TestSalesInvoice(unittest.TestCase):
"_Test Account Education Cess - _TC": [1.4, 1.4, 1.4],
"_Test Account S&H Education Cess - _TC": [0.7, 0.7, 0.7],
"_Test Account CST - _TC": [17.19, 17.19, 17.19],
- "_Test Account VAT - _TC": [78.13, 78.13, 78.13],
+ "_Test Account VAT - _TC": [78.12, 78.12, 78.12],
"_Test Account Discount - _TC": [-95.49, -95.49, -95.49],
}
@@ -1623,9 +1623,9 @@ class TestSalesInvoice(unittest.TestCase):
if expected_values.get(d.account_head):
self.assertEqual(d.get(k), expected_values[d.account_head][i])
- self.assertEqual(si.total_taxes_and_charges, 234.43)
- self.assertEqual(si.base_grand_total, 859.43)
- self.assertEqual(si.grand_total, 859.43)
+ self.assertEqual(si.total_taxes_and_charges, 234.42)
+ self.assertEqual(si.base_grand_total, 859.42)
+ self.assertEqual(si.grand_total, 859.42)
def test_multi_currency_gle(self):
si = create_sales_invoice(
@@ -1985,17 +1985,17 @@ class TestSalesInvoice(unittest.TestCase):
)
si.save()
si.submit()
- self.assertEqual(si.net_total, 19453.13)
+ self.assertEqual(si.net_total, 19453.12)
self.assertEqual(si.grand_total, 24900)
self.assertEqual(si.total_taxes_and_charges, 5446.88)
- self.assertEqual(si.rounding_adjustment, -0.01)
+ self.assertEqual(si.rounding_adjustment, 0.0)
expected_values = dict(
(d[0], d)
for d in [
[si.debit_to, 24900, 0.0],
["_Test Account Service Tax - _TC", 0.0, 5446.88],
- ["Sales - _TC", 0.0, 19453.13],
+ ["Sales - _TC", 0.0, 19453.12],
["Round Off - _TC", 0.01, 0.0],
]
)
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index 01cfb58dec..b217f00065 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -32,6 +32,16 @@ from erpnext import get_company_currency
from erpnext.accounts.utils import get_fiscal_year
from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen
+PURCHASE_TRANSACTION_TYPES = {"Purchase Order", "Purchase Receipt", "Purchase Invoice"}
+SALES_TRANSACTION_TYPES = {
+ "Quotation",
+ "Sales Order",
+ "Delivery Note",
+ "Sales Invoice",
+ "POS Invoice",
+}
+TRANSACTION_TYPES = PURCHASE_TRANSACTION_TYPES | SALES_TRANSACTION_TYPES
+
class DuplicatePartyAccountError(frappe.ValidationError):
pass
@@ -124,12 +134,6 @@ def _get_party_details(
set_other_values(party_details, party, party_type)
set_price_list(party_details, party, party_type, price_list, pos_profile)
- party_details["tax_category"] = get_address_tax_category(
- party.get("tax_category"),
- party_address,
- shipping_address if party_type != "Supplier" else party_address,
- )
-
tax_template = set_taxes(
party.name,
party_type,
@@ -211,20 +215,10 @@ def set_address_details(
else:
party_details.update(get_company_address(company))
- if doctype and doctype in [
- "Delivery Note",
- "Sales Invoice",
- "Sales Order",
- "Quotation",
- "POS Invoice",
- ]:
- if party_details.company_address:
- party_details.update(
- get_fetch_values(doctype, "company_address", party_details.company_address)
- )
- get_regional_address_details(party_details, doctype, company)
+ if doctype in SALES_TRANSACTION_TYPES and party_details.company_address:
+ party_details.update(get_fetch_values(doctype, "company_address", party_details.company_address))
- elif doctype and doctype in ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]:
+ if doctype in PURCHASE_TRANSACTION_TYPES:
if shipping_address:
party_details.update(
shipping_address=shipping_address,
@@ -250,9 +244,21 @@ def set_address_details(
**get_fetch_values(doctype, "shipping_address", party_details.billing_address)
)
+ party_address, shipping_address = (
+ party_details.get(billing_address_field),
+ party_details.shipping_address_name,
+ )
+
+ party_details["tax_category"] = get_address_tax_category(
+ party.get("tax_category"),
+ party_address,
+ shipping_address if party_type != "Supplier" else party_address,
+ )
+
+ if doctype in TRANSACTION_TYPES:
get_regional_address_details(party_details, doctype, company)
- return party_details.get(billing_address_field), party_details.shipping_address_name
+ return party_address, shipping_address
@erpnext.allow_regional
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index 94a1510f09..11de9a098d 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -859,7 +859,7 @@ class ReceivablePayableReport(object):
)
else:
self.qb_selection_filter.append(
- self.ple[dimension.fieldname] == self.filters[dimension.fieldname]
+ self.ple[dimension.fieldname].isin(self.filters[dimension.fieldname])
)
def is_invoice(self, ple):
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py
index fde4de8402..01fee281b0 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/gross_profit.py
@@ -501,7 +501,14 @@ class GrossProfitGenerator(object):
):
returned_item_rows = self.returned_invoices[row.parent][row.item_code]
for returned_item_row in returned_item_rows:
- row.qty += flt(returned_item_row.qty)
+ # returned_items 'qty' should be stateful
+ if returned_item_row.qty != 0:
+ if row.qty >= abs(returned_item_row.qty):
+ row.qty += returned_item_row.qty
+ returned_item_row.qty = 0
+ else:
+ row.qty = 0
+ returned_item_row.qty += row.qty
row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
if flt(row.qty) or row.base_amount:
@@ -734,6 +741,8 @@ class GrossProfitGenerator(object):
if self.filters.to_date:
conditions += " and posting_date <= %(to_date)s"
+ conditions += " and (is_return = 0 or (is_return=1 and return_against is null))"
+
if self.filters.item_group:
conditions += " and {0}".format(get_item_group_condition(self.filters.item_group))
diff --git a/erpnext/accounts/report/gross_profit/test_gross_profit.py b/erpnext/accounts/report/gross_profit/test_gross_profit.py
index 21681bef5b..82fe1a0ba1 100644
--- a/erpnext/accounts/report/gross_profit/test_gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/test_gross_profit.py
@@ -381,3 +381,82 @@ class TestGrossProfit(FrappeTestCase):
}
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
self.assertDictContainsSubset(expected_entry, gp_entry[0])
+
+ def test_crnote_against_invoice_with_multiple_instances_of_same_item(self):
+ """
+ Item Qty for Sales Invoices with multiple instances of same item go in the -ve. Ideally, the credit noteshould cancel out the invoice items.
+ """
+ from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
+
+ # Invoice with an item added twice
+ sinv = self.create_sales_invoice(qty=1, rate=100, posting_date=nowdate(), do_not_submit=True)
+ sinv.append("items", frappe.copy_doc(sinv.items[0], ignore_no_copy=False))
+ sinv = sinv.save().submit()
+
+ # Create Credit Note for Invoice
+ cr_note = make_sales_return(sinv.name)
+ cr_note = cr_note.save().submit()
+
+ filters = frappe._dict(
+ company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
+ )
+
+ columns, data = execute(filters=filters)
+ expected_entry = {
+ "parent_invoice": sinv.name,
+ "currency": "INR",
+ "sales_invoice": self.item,
+ "customer": self.customer,
+ "posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
+ "item_code": self.item,
+ "item_name": self.item,
+ "warehouse": "Stores - _GP",
+ "qty": 0.0,
+ "avg._selling_rate": 0.0,
+ "valuation_rate": 0.0,
+ "selling_amount": -100.0,
+ "buying_amount": 0.0,
+ "gross_profit": -100.0,
+ "gross_profit_%": 100.0,
+ }
+ gp_entry = [x for x in data if x.parent_invoice == sinv.name]
+ # Both items of Invoice should have '0' qty
+ self.assertEqual(len(gp_entry), 2)
+ self.assertDictContainsSubset(expected_entry, gp_entry[0])
+ self.assertDictContainsSubset(expected_entry, gp_entry[1])
+
+ def test_standalone_cr_notes(self):
+ """
+ Standalone cr notes will be reported as usual
+ """
+ # Make Cr Note
+ sinv = self.create_sales_invoice(
+ qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
+ )
+ sinv.is_return = 1
+ sinv = sinv.save().submit()
+
+ filters = frappe._dict(
+ company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
+ )
+
+ columns, data = execute(filters=filters)
+ expected_entry = {
+ "parent_invoice": sinv.name,
+ "currency": "INR",
+ "sales_invoice": self.item,
+ "customer": self.customer,
+ "posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
+ "item_code": self.item,
+ "item_name": self.item,
+ "warehouse": "Stores - _GP",
+ "qty": -1.0,
+ "avg._selling_rate": 100.0,
+ "valuation_rate": 0.0,
+ "selling_amount": -100.0,
+ "buying_amount": 0.0,
+ "gross_profit": -100.0,
+ "gross_profit_%": 100.0,
+ }
+ gp_entry = [x for x in data if x.parent_invoice == sinv.name]
+ self.assertDictContainsSubset(expected_entry, gp_entry[0])
diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py
index 3af01fde7d..bc334c7060 100644
--- a/erpnext/accounts/report/trial_balance/trial_balance.py
+++ b/erpnext/accounts/report/trial_balance/trial_balance.py
@@ -78,7 +78,6 @@ def validate_filters(filters):
def get_data(filters):
-
accounts = frappe.db.sql(
"""select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt
@@ -118,12 +117,10 @@ def get_data(filters):
ignore_closing_entries=not flt(filters.with_period_closing_entry),
)
- total_row = calculate_values(
- accounts, gl_entries_by_account, opening_balances, filters, company_currency
- )
+ calculate_values(accounts, gl_entries_by_account, opening_balances)
accumulate_values_into_parents(accounts, accounts_by_name)
- data = prepare_data(accounts, filters, total_row, parent_children_map, company_currency)
+ data = prepare_data(accounts, filters, parent_children_map, company_currency)
data = filter_out_zero_value_rows(
data, parent_children_map, show_zero_values=filters.get("show_zero_values")
)
@@ -218,7 +215,7 @@ def get_rootwise_opening_balances(filters, report_type):
return opening
-def calculate_values(accounts, gl_entries_by_account, opening_balances, filters, company_currency):
+def calculate_values(accounts, gl_entries_by_account, opening_balances):
init = {
"opening_debit": 0.0,
"opening_credit": 0.0,
@@ -228,22 +225,6 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters,
"closing_credit": 0.0,
}
- total_row = {
- "account": "'" + _("Total") + "'",
- "account_name": "'" + _("Total") + "'",
- "warn_if_negative": True,
- "opening_debit": 0.0,
- "opening_credit": 0.0,
- "debit": 0.0,
- "credit": 0.0,
- "closing_debit": 0.0,
- "closing_credit": 0.0,
- "parent_account": None,
- "indent": 0,
- "has_value": True,
- "currency": company_currency,
- }
-
for d in accounts:
d.update(init.copy())
@@ -261,8 +242,28 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters,
prepare_opening_closing(d)
- for field in value_fields:
- total_row[field] += d[field]
+
+def calculate_total_row(accounts, company_currency):
+ total_row = {
+ "account": "'" + _("Total") + "'",
+ "account_name": "'" + _("Total") + "'",
+ "warn_if_negative": True,
+ "opening_debit": 0.0,
+ "opening_credit": 0.0,
+ "debit": 0.0,
+ "credit": 0.0,
+ "closing_debit": 0.0,
+ "closing_credit": 0.0,
+ "parent_account": None,
+ "indent": 0,
+ "has_value": True,
+ "currency": company_currency,
+ }
+
+ for d in accounts:
+ if not d.parent_account:
+ for field in value_fields:
+ total_row[field] += d[field]
return total_row
@@ -274,7 +275,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name):
accounts_by_name[d.parent_account][key] += d[key]
-def prepare_data(accounts, filters, total_row, parent_children_map, company_currency):
+def prepare_data(accounts, filters, parent_children_map, company_currency):
data = []
for d in accounts:
@@ -305,6 +306,7 @@ def prepare_data(accounts, filters, total_row, parent_children_map, company_curr
row["has_value"] = has_value
data.append(row)
+ total_row = calculate_total_row(accounts, company_currency)
data.extend([{}, total_row])
return data
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 2608c03ffe..005a2f176c 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -455,7 +455,9 @@ def reconcile_against_document(args): # nosemgrep
try:
doc.validate_total_debit_and_credit()
except Exception as validation_exception:
- raise frappe.ValidationError(_(f"Validation Error for {doc.name}")) from validation_exception
+ raise frappe.ValidationError(
+ _("Validation Error for {0}").format(doc.name)
+ ) from validation_exception
doc.save(ignore_permissions=True)
# re-submit advance entry
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index 9a152638f9..2c9772d12a 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -828,8 +828,8 @@ class TestDepreciationMethods(AssetSetup):
expected_schedules = [
["2030-12-31", 28630.14, 28630.14],
["2031-12-31", 35684.93, 64315.07],
- ["2032-12-31", 17842.47, 82157.54],
- ["2033-06-06", 5342.46, 87500.0],
+ ["2032-12-31", 17842.46, 82157.53],
+ ["2033-06-06", 5342.47, 87500.0],
]
schedules = [
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
index b75fbcbeb3..5912329846 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
@@ -140,8 +140,8 @@ class AssetDepreciationSchedule(Document):
self.asset = asset_doc.name
self.finance_book = row.finance_book
self.finance_book_id = row.idx
- self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation
- self.number_of_depreciations_booked = asset_doc.number_of_depreciations_booked
+ self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation or 0
+ self.number_of_depreciations_booked = asset_doc.number_of_depreciations_booked or 0
self.gross_purchase_amount = asset_doc.gross_purchase_amount
self.depreciation_method = row.depreciation_method
self.total_number_of_depreciations = row.total_number_of_depreciations
@@ -185,14 +185,14 @@ class AssetDepreciationSchedule(Document):
):
asset_doc.validate_asset_finance_books(row)
- value_after_depreciation = _get_value_after_depreciation_for_making_schedule(asset_doc, row)
+ value_after_depreciation = self._get_value_after_depreciation_for_making_schedule(asset_doc, row)
row.value_after_depreciation = value_after_depreciation
if update_asset_finance_book_row:
row.db_update()
number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
- asset_doc.number_of_depreciations_booked
+ self.number_of_depreciations_booked
)
has_pro_rata = asset_doc.check_is_pro_rata(row)
@@ -235,13 +235,12 @@ class AssetDepreciationSchedule(Document):
self.add_depr_schedule_row(
date_of_disposal,
depreciation_amount,
- row.depreciation_method,
)
break
# For first row
- if has_pro_rata and not asset_doc.opening_accumulated_depreciation and n == 0:
+ if has_pro_rata and not self.opening_accumulated_depreciation and n == 0:
from_date = add_days(
asset_doc.available_for_use_date, -1
) # needed to calc depr amount for available_for_use_date too
@@ -260,7 +259,7 @@ class AssetDepreciationSchedule(Document):
# In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission
asset_doc.to_date = add_months(
asset_doc.available_for_use_date,
- (n + asset_doc.number_of_depreciations_booked) * cint(row.frequency_of_depreciation),
+ (n + self.number_of_depreciations_booked) * cint(row.frequency_of_depreciation),
)
depreciation_amount_without_pro_rata = depreciation_amount
@@ -298,7 +297,6 @@ class AssetDepreciationSchedule(Document):
self.add_depr_schedule_row(
schedule_date,
depreciation_amount,
- row.depreciation_method,
)
# to ensure that final accumulated depreciation amount is accurate
@@ -325,14 +323,12 @@ class AssetDepreciationSchedule(Document):
self,
schedule_date,
depreciation_amount,
- depreciation_method,
):
self.append(
"depreciation_schedule",
{
"schedule_date": schedule_date,
"depreciation_amount": depreciation_amount,
- "depreciation_method": depreciation_method,
},
)
@@ -346,7 +342,7 @@ class AssetDepreciationSchedule(Document):
straight_line_idx = [
d.idx
for d in self.get("depreciation_schedule")
- if d.depreciation_method == "Straight Line" or d.depreciation_method == "Manual"
+ if self.depreciation_method == "Straight Line" or self.depreciation_method == "Manual"
]
accumulated_depreciation = flt(self.opening_accumulated_depreciation)
@@ -377,16 +373,15 @@ class AssetDepreciationSchedule(Document):
accumulated_depreciation, d.precision("accumulated_depreciation_amount")
)
+ def _get_value_after_depreciation_for_making_schedule(self, asset_doc, fb_row):
+ if asset_doc.docstatus == 1 and fb_row.value_after_depreciation:
+ value_after_depreciation = flt(fb_row.value_after_depreciation)
+ else:
+ value_after_depreciation = flt(self.gross_purchase_amount) - flt(
+ self.opening_accumulated_depreciation
+ )
-def _get_value_after_depreciation_for_making_schedule(asset_doc, fb_row):
- if asset_doc.docstatus == 1 and fb_row.value_after_depreciation:
- value_after_depreciation = flt(fb_row.value_after_depreciation)
- else:
- value_after_depreciation = flt(asset_doc.gross_purchase_amount) - flt(
- asset_doc.opening_accumulated_depreciation
- )
-
- return value_after_depreciation
+ return value_after_depreciation
def make_draft_asset_depr_schedules_if_not_present(asset_doc):
diff --git a/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json b/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json
index 882c4bf00b..abe295c680 100644
--- a/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json
+++ b/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json
@@ -12,8 +12,7 @@
"column_break_3",
"accumulated_depreciation_amount",
"journal_entry",
- "make_depreciation_entry",
- "depreciation_method"
+ "make_depreciation_entry"
],
"fields": [
{
@@ -58,20 +57,11 @@
"fieldname": "make_depreciation_entry",
"fieldtype": "Button",
"label": "Make Depreciation Entry"
- },
- {
- "fieldname": "depreciation_method",
- "fieldtype": "Select",
- "hidden": 1,
- "label": "Depreciation Method",
- "options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual",
- "print_hide": 1,
- "read_only": 1
}
],
"istable": 1,
"links": [],
- "modified": "2022-12-06 20:35:50.264281",
+ "modified": "2023-03-13 23:17:15.849950",
"modified_by": "Administrator",
"module": "Assets",
"name": "Depreciation Schedule",
diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json
index 95857e4604..8c73e56a99 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.json
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -16,6 +16,7 @@
"transaction_settings_section",
"po_required",
"pr_required",
+ "over_order_allowance",
"column_break_12",
"maintain_same_rate",
"set_landed_cost_based_on_purchase_invoice_rate",
@@ -156,6 +157,13 @@
"fieldname": "set_landed_cost_based_on_purchase_invoice_rate",
"fieldtype": "Check",
"label": "Set Landed Cost Based on Purchase Invoice Rate"
+ },
+ {
+ "default": "0",
+ "description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.",
+ "fieldname": "over_order_allowance",
+ "fieldtype": "Float",
+ "label": "Over Order Allowance (%)"
}
],
"icon": "fa fa-cog",
@@ -163,7 +171,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2023-02-28 15:41:32.686805",
+ "modified": "2023-03-02 17:02:14.404622",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 2415aec8cb..06b9d29e69 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -21,6 +21,9 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
from erpnext.accounts.party import get_party_account, get_party_account_currency
from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items
from erpnext.controllers.buying_controller import BuyingController
+from erpnext.manufacturing.doctype.blanket_order.blanket_order import (
+ validate_against_blanket_order,
+)
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock.doctype.item.item import get_item_defaults, get_last_purchase_details
from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty
@@ -69,6 +72,7 @@ class PurchaseOrder(BuyingController):
self.validate_with_previous_doc()
self.validate_for_subcontracting()
self.validate_minimum_order_qty()
+ validate_against_blanket_order(self)
if self.is_old_subcontracting_flow:
self.validate_bom_for_subcontracting_items()
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
index 7927beb823..4590f8c3d9 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
@@ -113,7 +113,10 @@ class RequestforQuotation(BuyingController):
def get_link(self):
# RFQ link for supplier portal
- return get_url("/app/request-for-quotation/" + self.name)
+ route = frappe.db.get_value(
+ "Portal Menu Item", {"reference_doctype": "Request for Quotation"}, ["route"]
+ )
+ return get_url("/app/{0}/".format(route) + self.name)
def update_supplier_part_no(self, supplier):
self.vendor = supplier
diff --git a/erpnext/e_commerce/doctype/website_item/test_website_item.py b/erpnext/e_commerce/doctype/website_item/test_website_item.py
index bbe04d5514..e41c9da594 100644
--- a/erpnext/e_commerce/doctype/website_item/test_website_item.py
+++ b/erpnext/e_commerce/doctype/website_item/test_website_item.py
@@ -226,11 +226,11 @@ class TestWebsiteItem(unittest.TestCase):
self.assertTrue(bool(data.product_info["price"]))
price_object = data.product_info["price"]
- self.assertEqual(price_object.get("discount_percent"), 25)
+ self.assertEqual(price_object.get("discount_percent"), 25.0)
self.assertEqual(price_object.get("price_list_rate"), 750)
self.assertEqual(price_object.get("formatted_mrp"), "₹ 1,000.00")
self.assertEqual(price_object.get("formatted_price"), "₹ 750.00")
- self.assertEqual(price_object.get("formatted_discount_percent"), "25%")
+ self.assertEqual(price_object.get("formatted_discount_percent"), "25.0%")
# switch to admin and disable show price
frappe.set_user("Administrator")
diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.js b/erpnext/manufacturing/doctype/blanket_order/blanket_order.js
index d3bb33e86e..7b26a14a57 100644
--- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.js
+++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.js
@@ -7,6 +7,12 @@ frappe.ui.form.on('Blanket Order', {
},
setup: function(frm) {
+ frm.custom_make_buttons = {
+ 'Purchase Order': 'Purchase Order',
+ 'Sales Order': 'Sales Order',
+ 'Quotation': 'Quotation',
+ };
+
frm.add_fetch("customer", "customer_name", "customer_name");
frm.add_fetch("supplier", "supplier_name", "supplier_name");
},
diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py
index ff2140199d..32f1c365ad 100644
--- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py
+++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py
@@ -6,6 +6,7 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
+from frappe.query_builder.functions import Sum
from frappe.utils import flt, getdate
from erpnext.stock.doctype.item.item import get_item_defaults
@@ -29,21 +30,23 @@ class BlanketOrder(Document):
def update_ordered_qty(self):
ref_doctype = "Sales Order" if self.blanket_order_type == "Selling" else "Purchase Order"
+
+ trans = frappe.qb.DocType(ref_doctype)
+ trans_item = frappe.qb.DocType(f"{ref_doctype} Item")
+
item_ordered_qty = frappe._dict(
- frappe.db.sql(
- """
- select trans_item.item_code, sum(trans_item.stock_qty) as qty
- from `tab{0} Item` trans_item, `tab{0}` trans
- where trans.name = trans_item.parent
- and trans_item.blanket_order=%s
- and trans.docstatus=1
- and trans.status not in ('Closed', 'Stopped')
- group by trans_item.item_code
- """.format(
- ref_doctype
- ),
- self.name,
- )
+ (
+ frappe.qb.from_(trans_item)
+ .from_(trans)
+ .select(trans_item.item_code, Sum(trans_item.stock_qty).as_("qty"))
+ .where(
+ (trans.name == trans_item.parent)
+ & (trans_item.blanket_order == self.name)
+ & (trans.docstatus == 1)
+ & (trans.status.notin(["Stopped", "Closed"]))
+ )
+ .groupby(trans_item.item_code)
+ ).run()
)
for d in self.items:
@@ -79,7 +82,43 @@ def make_order(source_name):
"doctype": doctype + " Item",
"field_map": {"rate": "blanket_order_rate", "parent": "blanket_order"},
"postprocess": update_item,
+ "condition": lambda item: (flt(item.qty) - flt(item.ordered_qty)) > 0,
},
},
)
return target_doc
+
+
+def validate_against_blanket_order(order_doc):
+ if order_doc.doctype in ("Sales Order", "Purchase Order"):
+ order_data = {}
+
+ for item in order_doc.get("items"):
+ if item.against_blanket_order and item.blanket_order:
+ if item.blanket_order in order_data:
+ if item.item_code in order_data[item.blanket_order]:
+ order_data[item.blanket_order][item.item_code] += item.qty
+ else:
+ order_data[item.blanket_order][item.item_code] = item.qty
+ else:
+ order_data[item.blanket_order] = {item.item_code: item.qty}
+
+ if order_data:
+ allowance = flt(
+ frappe.db.get_single_value(
+ "Selling Settings" if order_doc.doctype == "Sales Order" else "Buying Settings",
+ "over_order_allowance",
+ )
+ )
+ for bo_name, item_data in order_data.items():
+ bo_doc = frappe.get_doc("Blanket Order", bo_name)
+ for item in bo_doc.get("items"):
+ if item.item_code in item_data:
+ remaining_qty = item.qty - item.ordered_qty
+ allowed_qty = remaining_qty + (remaining_qty * (allowance / 100))
+ if allowed_qty < item_data[item.item_code]:
+ frappe.throw(
+ _("Item {0} cannot be ordered more than {1} against Blanket Order {2}.").format(
+ item.item_code, allowed_qty, bo_name
+ )
+ )
diff --git a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py
index 2f1f3ae0f5..58f3c95059 100644
--- a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py
+++ b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py
@@ -63,6 +63,33 @@ class TestBlanketOrder(FrappeTestCase):
po1.currency = get_company_currency(po1.company)
self.assertEqual(po1.items[0].qty, (bo.items[0].qty - bo.items[0].ordered_qty))
+ def test_over_order_allowance(self):
+ # Sales Order
+ bo = make_blanket_order(blanket_order_type="Selling", quantity=100)
+
+ frappe.flags.args.doctype = "Sales Order"
+ so = make_order(bo.name)
+ so.currency = get_company_currency(so.company)
+ so.delivery_date = today()
+ so.items[0].qty = 110
+ self.assertRaises(frappe.ValidationError, so.submit)
+
+ frappe.db.set_single_value("Selling Settings", "over_order_allowance", 10)
+ so.submit()
+
+ # Purchase Order
+ bo = make_blanket_order(blanket_order_type="Purchasing", quantity=100)
+
+ frappe.flags.args.doctype = "Purchase Order"
+ po = make_order(bo.name)
+ po.currency = get_company_currency(po.company)
+ po.schedule_date = today()
+ po.items[0].qty = 110
+ self.assertRaises(frappe.ValidationError, po.submit)
+
+ frappe.db.set_single_value("Buying Settings", "over_order_allowance", 10)
+ po.submit()
+
def make_blanket_order(**args):
args = frappe._dict(args)
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 8ab79e68be..619a415c8b 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -31,7 +31,7 @@ class BOMTree:
# specifying the attributes to save resources
# ref: https://docs.python.org/3/reference/datamodel.html#slots
- __slots__ = ["name", "child_items", "is_bom", "item_code", "exploded_qty", "qty"]
+ __slots__ = ["name", "child_items", "is_bom", "item_code", "qty", "exploded_qty", "bom_qty"]
def __init__(
self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1
@@ -50,9 +50,10 @@ class BOMTree:
def __create_tree(self):
bom = frappe.get_cached_doc("BOM", self.name)
self.item_code = bom.item
+ self.bom_qty = bom.quantity
for item in bom.get("items", []):
- qty = item.qty / bom.quantity # quantity per unit
+ qty = item.stock_qty / bom.quantity # quantity per unit
exploded_qty = self.exploded_qty * qty
if item.bom_no:
child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty)
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index d60feb2b39..01bf2e4315 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -6,7 +6,7 @@ from collections import deque
from functools import partial
import frappe
-from frappe.tests.utils import FrappeTestCase
+from frappe.tests.utils import FrappeTestCase, timeout
from frappe.utils import cstr, flt
from erpnext.controllers.tests.test_subcontracting_controller import (
@@ -27,6 +27,7 @@ test_dependencies = ["Item", "Quality Inspection Template"]
class TestBOM(FrappeTestCase):
+ @timeout
def test_get_items(self):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
@@ -37,6 +38,7 @@ class TestBOM(FrappeTestCase):
self.assertTrue(test_records[2]["items"][1]["item_code"] in items_dict)
self.assertEqual(len(items_dict.values()), 2)
+ @timeout
def test_get_items_exploded(self):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
@@ -49,11 +51,13 @@ class TestBOM(FrappeTestCase):
self.assertTrue(test_records[0]["items"][1]["item_code"] in items_dict)
self.assertEqual(len(items_dict.values()), 3)
+ @timeout
def test_get_items_list(self):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items
self.assertEqual(len(get_bom_items(bom=get_default_bom(), company="_Test Company")), 3)
+ @timeout
def test_default_bom(self):
def _get_default_bom_in_item():
return cstr(frappe.db.get_value("Item", "_Test FG Item 2", "default_bom"))
@@ -71,6 +75,7 @@ class TestBOM(FrappeTestCase):
self.assertTrue(_get_default_bom_in_item(), bom.name)
+ @timeout
def test_update_bom_cost_in_all_boms(self):
# get current rate for '_Test Item 2'
bom_rates = frappe.db.get_values(
@@ -99,6 +104,7 @@ class TestBOM(FrappeTestCase):
):
self.assertEqual(d.base_rate, rm_base_rate + 10)
+ @timeout
def test_bom_cost(self):
bom = frappe.copy_doc(test_records[2])
bom.insert()
@@ -127,6 +133,7 @@ class TestBOM(FrappeTestCase):
self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost)
self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost)
+ @timeout
def test_bom_cost_with_batch_size(self):
bom = frappe.copy_doc(test_records[2])
bom.docstatus = 0
@@ -145,6 +152,7 @@ class TestBOM(FrappeTestCase):
self.assertAlmostEqual(bom.operating_cost, op_cost / 2)
bom.delete()
+ @timeout
def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self):
frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependent", 1)
for item_code, rate in (("_Test Item", 3600), ("_Test Item Home Desktop Manufactured", 3000)):
@@ -181,6 +189,7 @@ class TestBOM(FrappeTestCase):
self.assertEqual(bom.base_raw_material_cost, 27000)
self.assertEqual(bom.base_total_cost, 33000)
+ @timeout
def test_bom_cost_multi_uom_based_on_valuation_rate(self):
bom = frappe.copy_doc(test_records[2])
bom.set_rate_of_sub_assembly_item_based_on_bom = 0
@@ -202,6 +211,7 @@ class TestBOM(FrappeTestCase):
self.assertEqual(bom.items[0].rate, 20)
+ @timeout
def test_bom_cost_with_fg_based_operating_cost(self):
bom = frappe.copy_doc(test_records[4])
bom.insert()
@@ -229,6 +239,7 @@ class TestBOM(FrappeTestCase):
self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost)
self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost)
+ @timeout
def test_subcontractor_sourced_item(self):
item_code = "_Test Subcontracted FG Item 1"
set_backflush_based_on("Material Transferred for Subcontract")
@@ -310,6 +321,7 @@ class TestBOM(FrappeTestCase):
supplied_items = sorted([d.rm_item_code for d in sco.supplied_items])
self.assertEqual(bom_items, supplied_items)
+ @timeout
def test_bom_tree_representation(self):
bom_tree = {
"Assembly": {
@@ -335,6 +347,7 @@ class TestBOM(FrappeTestCase):
for reqd_item, created_item in zip(reqd_order, created_order):
self.assertEqual(reqd_item, created_item.item_code)
+ @timeout
def test_generated_variant_bom(self):
from erpnext.controllers.item_variant import create_variant
@@ -375,6 +388,7 @@ class TestBOM(FrappeTestCase):
self.assertEqual(reqd_item.qty, created_item.qty)
self.assertEqual(reqd_item.exploded_qty, created_item.exploded_qty)
+ @timeout
def test_bom_recursion_1st_level(self):
"""BOM should not allow BOM item again in child"""
item_code = make_item(properties={"is_stock_item": 1}).name
@@ -387,6 +401,7 @@ class TestBOM(FrappeTestCase):
bom.items[0].bom_no = bom.name
bom.save()
+ @timeout
def test_bom_recursion_transitive(self):
item1 = make_item(properties={"is_stock_item": 1}).name
item2 = make_item(properties={"is_stock_item": 1}).name
@@ -408,6 +423,7 @@ class TestBOM(FrappeTestCase):
bom1.save()
bom2.save()
+ @timeout
def test_bom_with_process_loss_item(self):
fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items()
@@ -421,6 +437,7 @@ class TestBOM(FrappeTestCase):
# Items with whole UOMs can't be PL Items
self.assertRaises(frappe.ValidationError, bom_doc.submit)
+ @timeout
def test_bom_item_query(self):
query = partial(
item_query,
@@ -440,6 +457,7 @@ class TestBOM(FrappeTestCase):
)
self.assertTrue(0 < len(filtered) <= 3, msg="Item filtering showing excessive results")
+ @timeout
def test_exclude_exploded_items_from_bom(self):
bom_no = get_default_bom()
new_bom = frappe.copy_doc(frappe.get_doc("BOM", bom_no))
@@ -458,6 +476,7 @@ class TestBOM(FrappeTestCase):
new_bom.delete()
+ @timeout
def test_valid_transfer_defaults(self):
bom_with_op = frappe.db.get_value(
"BOM", {"item": "_Test FG Item 2", "with_operations": 1, "is_active": 1}
@@ -489,11 +508,13 @@ class TestBOM(FrappeTestCase):
self.assertEqual(bom.transfer_material_against, "Work Order")
bom.delete()
+ @timeout
def test_bom_name_length(self):
"""test >140 char names"""
bom_tree = {"x" * 140: {" ".join(["abc"] * 35): {}}}
create_nested_bom(bom_tree, prefix="")
+ @timeout
def test_version_index(self):
bom = frappe.new_doc("BOM")
@@ -515,6 +536,7 @@ class TestBOM(FrappeTestCase):
msg=f"Incorrect index for {existing_boms}",
)
+ @timeout
def test_bom_versioning(self):
bom_tree = {frappe.generate_hash(length=10): {frappe.generate_hash(length=10): {}}}
bom = create_nested_bom(bom_tree, prefix="")
@@ -547,6 +569,7 @@ class TestBOM(FrappeTestCase):
self.assertNotEqual(amendment.name, version.name)
self.assertEqual(int(version.name.split("-")[-1]), 2)
+ @timeout
def test_clear_inpection_quality(self):
bom = frappe.copy_doc(test_records[2], ignore_no_copy=True)
@@ -565,6 +588,7 @@ class TestBOM(FrappeTestCase):
self.assertEqual(bom.quality_inspection_template, None)
+ @timeout
def test_bom_pricing_based_on_lpp(self):
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
@@ -585,6 +609,7 @@ class TestBOM(FrappeTestCase):
bom.submit()
self.assertEqual(bom.items[0].rate, 42)
+ @timeout
def test_set_default_bom_for_item_having_single_bom(self):
from erpnext.stock.doctype.item.test_item import make_item
@@ -621,6 +646,7 @@ class TestBOM(FrappeTestCase):
bom.reload()
self.assertEqual(frappe.get_value("Item", fg_item.item_code, "default_bom"), bom.name)
+ @timeout
def test_exploded_items_rate(self):
rm_item = make_item(
properties={"is_stock_item": 1, "valuation_rate": 99, "last_purchase_rate": 89}
@@ -649,6 +675,7 @@ class TestBOM(FrappeTestCase):
bom.submit()
self.assertEqual(bom.exploded_items[0].rate, bom.items[0].base_rate)
+ @timeout
def test_bom_cost_update_flag(self):
rm_item = make_item(
properties={"is_stock_item": 1, "valuation_rate": 99, "last_purchase_rate": 89}
diff --git a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py
index 5dd557f8ab..2026f62914 100644
--- a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py
+++ b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py
@@ -2,7 +2,7 @@
# License: GNU General Public License v3. See license.txt
import frappe
-from frappe.tests.utils import FrappeTestCase
+from frappe.tests.utils import FrappeTestCase, timeout
from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import (
update_cost_in_all_boms_in_test,
@@ -20,6 +20,7 @@ class TestBOMUpdateTool(FrappeTestCase):
def tearDown(self):
frappe.db.rollback()
+ @timeout
def test_replace_bom(self):
current_bom = "BOM-_Test Item Home Desktop Manufactured-001"
@@ -33,6 +34,7 @@ class TestBOMUpdateTool(FrappeTestCase):
self.assertFalse(frappe.db.exists("BOM Item", {"bom_no": current_bom, "docstatus": 1}))
self.assertTrue(frappe.db.exists("BOM Item", {"bom_no": bom_doc.name, "docstatus": 1}))
+ @timeout
def test_bom_cost(self):
for item in ["BOM Cost Test Item 1", "BOM Cost Test Item 2", "BOM Cost Test Item 3"]:
item_doc = create_item(item, valuation_rate=100)
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index ae9e9c6962..66b871c746 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -682,7 +682,7 @@ class WorkOrder(Document):
for node in bom_traversal:
if node.is_bom:
- operations.extend(_get_operations(node.name, qty=node.exploded_qty))
+ operations.extend(_get_operations(node.name, qty=node.exploded_qty / node.bom_qty))
bom_qty = frappe.get_cached_value("BOM", self.bom_no, "quantity")
operations.extend(_get_operations(self.bom_no, qty=1.0 / bom_qty))
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index e4995f8203..d42b012466 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -322,6 +322,8 @@ erpnext.patches.v14_0.create_accounting_dimensions_for_payment_request
erpnext.patches.v14_0.update_entry_type_for_journal_entry
erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers
erpnext.patches.v14_0.set_pick_list_status
+erpnext.patches.v13_0.update_docs_link
erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries
+erpnext.patches.v15_0.update_gpa_and_ndb_for_assdeprsch
# below migration patches should always run last
-erpnext.patches.v14_0.migrate_gl_to_payment_ledger
+erpnext.patches.v14_0.migrate_gl_to_payment_ledger
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/update_docs_link.py b/erpnext/patches/v13_0/update_docs_link.py
new file mode 100644
index 0000000000..4bc5c053d2
--- /dev/null
+++ b/erpnext/patches/v13_0/update_docs_link.py
@@ -0,0 +1,14 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# License: MIT. See LICENSE
+
+
+import frappe
+
+
+def execute():
+ navbar_settings = frappe.get_single("Navbar Settings")
+ for item in navbar_settings.help_dropdown:
+ if item.is_standard and item.route == "https://erpnext.com/docs/user/manual":
+ item.route = "https://docs.erpnext.com/docs/v14/user/manual/en/introduction"
+
+ navbar_settings.save()
diff --git a/erpnext/patches/v14_0/update_opportunity_currency_fields.py b/erpnext/patches/v14_0/update_opportunity_currency_fields.py
index b803e9fa2d..af736919d8 100644
--- a/erpnext/patches/v14_0/update_opportunity_currency_fields.py
+++ b/erpnext/patches/v14_0/update_opportunity_currency_fields.py
@@ -7,6 +7,9 @@ from erpnext.setup.utils import get_exchange_rate
def execute():
+ frappe.reload_doc(
+ "accounts", "doctype", "currency_exchange_settings"
+ ) # get_exchange_rate depends on Currency Exchange Settings
frappe.reload_doctype("Opportunity")
opportunities = frappe.db.get_list(
"Opportunity",
diff --git a/erpnext/patches/v15_0/update_gpa_and_ndb_for_assdeprsch.py b/erpnext/patches/v15_0/update_gpa_and_ndb_for_assdeprsch.py
new file mode 100644
index 0000000000..afb59e0f6f
--- /dev/null
+++ b/erpnext/patches/v15_0/update_gpa_and_ndb_for_assdeprsch.py
@@ -0,0 +1,20 @@
+import frappe
+
+
+def execute():
+ # not using frappe.qb because https://github.com/frappe/frappe/issues/20292
+ frappe.db.sql(
+ """UPDATE `tabAsset Depreciation Schedule`
+ JOIN `tabAsset`
+ ON `tabAsset Depreciation Schedule`.`asset`=`tabAsset`.`name`
+ SET
+ `tabAsset Depreciation Schedule`.`gross_purchase_amount`=`tabAsset`.`gross_purchase_amount`,
+ `tabAsset Depreciation Schedule`.`number_of_depreciations_booked`=`tabAsset`.`number_of_depreciations_booked`
+ WHERE
+ (
+ `tabAsset Depreciation Schedule`.`gross_purchase_amount`<>`tabAsset`.`gross_purchase_amount`
+ OR
+ `tabAsset Depreciation Schedule`.`number_of_depreciations_booked`<>`tabAsset`.`number_of_depreciations_booked`
+ )
+ AND `tabAsset Depreciation Schedule`.`docstatus`<2"""
+ )
diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js
index a376bf46a5..d1d07a79d6 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.js
+++ b/erpnext/projects/doctype/timesheet/timesheet.js
@@ -5,6 +5,8 @@ frappe.ui.form.on("Timesheet", {
setup: function(frm) {
frappe.require("/assets/erpnext/js/projects/timer.js");
+ frm.ignore_doctypes_on_cancel_all = ['Sales Invoice'];
+
frm.fields_dict.employee.get_query = function() {
return {
filters:{
diff --git a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py
index 17e3155e28..766e40e319 100644
--- a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py
+++ b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py
@@ -46,6 +46,9 @@ def get_data(filters):
# task has no end date, hence no delay
task.delay = 0
+ task.status = _(task.status)
+ task.priority = _(task.priority)
+
# Sort by descending order of delay
tasks.sort(key=lambda x: x["delay"], reverse=True)
return tasks
@@ -73,7 +76,7 @@ def get_chart_data(data):
on_track = on_track + 1
charts = {
"data": {
- "labels": ["On Track", "Delayed"],
+ "labels": [_("On Track"), _("Delayed")],
"datasets": [{"name": "Delayed", "values": [on_track, delay]}],
},
"type": "percentage",
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 385d0f3a58..ee9161bee4 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -21,6 +21,9 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
)
from erpnext.accounts.party import get_party_account
from erpnext.controllers.selling_controller import SellingController
+from erpnext.manufacturing.doctype.blanket_order.blanket_order import (
+ validate_against_blanket_order,
+)
from erpnext.manufacturing.doctype.production_plan.production_plan import (
get_items_for_material_requests,
)
@@ -52,6 +55,7 @@ class SalesOrder(SellingController):
self.validate_warehouse()
self.validate_drop_ship()
self.validate_serial_no_based_delivery()
+ validate_against_blanket_order(self)
validate_inter_company_party(
self.doctype, self.customer, self.company, self.inter_company_order_reference
)
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json
index 6ea66a0237..45ad7d95a1 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.json
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.json
@@ -24,6 +24,7 @@
"so_required",
"dn_required",
"sales_update_frequency",
+ "over_order_allowance",
"column_break_5",
"allow_multiple_items",
"allow_against_multiple_purchase_orders",
@@ -179,6 +180,12 @@
"fieldname": "allow_sales_order_creation_for_expired_quotation",
"fieldtype": "Check",
"label": "Allow Sales Order Creation For Expired Quotation"
+ },
+ {
+ "description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.",
+ "fieldname": "over_order_allowance",
+ "fieldtype": "Float",
+ "label": "Over Order Allowance (%)"
}
],
"icon": "fa fa-cog",
@@ -186,7 +193,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2023-02-04 12:37:53.380857",
+ "modified": "2023-03-03 11:16:54.333615",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling Settings",
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 07ee2890c4..fcdf245659 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -808,7 +808,7 @@ def get_default_company_address(name, sort_key="is_primary_address", existing_ad
return existing_address
if out:
- return min(out, key=lambda x: x[1])[0] # find min by sort_key
+ return max(out, key=lambda x: x[1])[0] # find max by sort_key
else:
return None
diff --git a/erpnext/setup/doctype/company/test_company.py b/erpnext/setup/doctype/company/test_company.py
index 29e056e34f..fd2fe300fa 100644
--- a/erpnext/setup/doctype/company/test_company.py
+++ b/erpnext/setup/doctype/company/test_company.py
@@ -11,6 +11,7 @@ from frappe.utils import random_string
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import (
get_charts_for_country,
)
+from erpnext.setup.doctype.company.company import get_default_company_address
test_ignore = ["Account", "Cost Center", "Payment Terms Template", "Salary Component", "Warehouse"]
test_dependencies = ["Fiscal Year"]
@@ -132,6 +133,38 @@ class TestCompany(unittest.TestCase):
self.assertTrue(lft >= min_lft)
self.assertTrue(rgt <= max_rgt)
+ def test_primary_address(self):
+ company = "_Test Company"
+
+ secondary = frappe.get_doc(
+ {
+ "address_title": "Non Primary",
+ "doctype": "Address",
+ "address_type": "Billing",
+ "address_line1": "Something",
+ "city": "Mumbai",
+ "state": "Maharashtra",
+ "country": "India",
+ "is_primary_address": 1,
+ "pincode": "400098",
+ "links": [
+ {
+ "link_doctype": "Company",
+ "link_name": company,
+ }
+ ],
+ }
+ )
+ secondary.insert()
+ self.addCleanup(secondary.delete)
+
+ primary = frappe.copy_doc(secondary)
+ primary.is_primary_address = 1
+ primary.insert()
+ self.addCleanup(primary.delete)
+
+ self.assertEqual(get_default_company_address(company), primary.name)
+
def get_no_of_children(self, company):
def get_no_of_children(companies, no_of_children):
children = []
diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py
index 1f7dddfb95..088958d1b2 100644
--- a/erpnext/setup/install.py
+++ b/erpnext/setup/install.py
@@ -155,7 +155,7 @@ def add_standard_navbar_items():
{
"item_label": "Documentation",
"item_type": "Route",
- "route": "https://erpnext.com/docs/user/manual",
+ "route": "https://docs.erpnext.com/docs/v14/user/manual/en/introduction",
"is_standard": 1,
},
{
diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
index 916ab2a05b..1763269193 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -636,7 +636,8 @@
"no_copy": 1,
"options": "Sales Invoice",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "search_index": 1
},
{
"fieldname": "so_detail",
@@ -837,7 +838,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2022-11-09 12:17:50.850142",
+ "modified": "2023-03-20 14:24:10.406746",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index c06700a99a..05a37ee4c4 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -377,7 +377,9 @@ class Item(Document):
"" if item_barcode.barcode_type not in options else item_barcode.barcode_type
)
if item_barcode.barcode_type:
- barcode_type = convert_erpnext_to_barcodenumber(item_barcode.barcode_type.upper())
+ barcode_type = convert_erpnext_to_barcodenumber(
+ item_barcode.barcode_type.upper(), item_barcode.barcode
+ )
if barcode_type in barcodenumber.barcodes():
if not barcodenumber.check_code(barcode_type, item_barcode.barcode):
frappe.throw(
@@ -982,20 +984,29 @@ class Item(Document):
)
-def convert_erpnext_to_barcodenumber(erpnext_number):
+def convert_erpnext_to_barcodenumber(erpnext_number, barcode):
+ if erpnext_number == "EAN":
+ ean_type = {
+ 8: "EAN8",
+ 13: "EAN13",
+ }
+ barcode_length = len(barcode)
+ if barcode_length in ean_type:
+ return ean_type[barcode_length]
+
+ return erpnext_number
+
convert = {
"UPC-A": "UPCA",
"CODE-39": "CODE39",
- "EAN": "EAN13",
- "EAN-12": "EAN",
- "EAN-8": "EAN8",
"ISBN-10": "ISBN10",
"ISBN-13": "ISBN13",
}
+
if erpnext_number in convert:
return convert[erpnext_number]
- else:
- return erpnext_number
+
+ return erpnext_number
def make_item_price(item, price_list_name, item_price):
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 67ed90d4e7..0c6dc77635 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -581,8 +581,9 @@ class TestItem(FrappeTestCase):
},
{"barcode": "72527273070", "barcode_type": "UPC-A"},
{"barcode": "123456", "barcode_type": "CODE-39"},
- {"barcode": "401268452363", "barcode_type": "EAN-12"},
- {"barcode": "90311017", "barcode_type": "EAN-8"},
+ {"barcode": "401268452363", "barcode_type": "EAN"},
+ {"barcode": "90311017", "barcode_type": "EAN"},
+ {"barcode": "73513537", "barcode_type": "EAN"},
{"barcode": "0123456789012", "barcode_type": "GS1"},
{"barcode": "2211564566668", "barcode_type": "GTIN"},
{"barcode": "0256480249", "barcode_type": "ISBN"},
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index bf3b5ddc54..46d6e9e757 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -172,8 +172,8 @@ class PickList(Document):
if (row.picked_qty / row.stock_qty) * 100 > over_delivery_receipt_allowance:
frappe.throw(
_(
- f"You are picking more than required quantity for the item {row.item_code}. Check if there is any other pick list created for the sales order {row.sales_order}."
- )
+ "You are picking more than required quantity for the item {0}. Check if there is any other pick list created for the sales order {1}."
+ ).format(row.item_code, row.sales_order)
)
@frappe.whitelist()
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 7f69397fce..36c875f308 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -658,6 +658,7 @@ class StockEntry(StockController):
)
finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item)
+ items = []
# Set basic rate for incoming items
for d in self.get("items"):
if d.s_warehouse or d.set_basic_rate_manually:
@@ -665,12 +666,7 @@ class StockEntry(StockController):
if d.allow_zero_valuation_rate:
d.basic_rate = 0.0
- frappe.msgprint(
- _(
- "Row {0}: Item rate has been updated to zero as Allow Zero Valuation Rate is checked for item {1}"
- ).format(d.idx, d.item_code),
- alert=1,
- )
+ items.append(d.item_code)
elif d.is_finished_item:
if self.purpose == "Manufacture":
@@ -697,6 +693,20 @@ class StockEntry(StockController):
d.basic_rate = flt(d.basic_rate)
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
+ if items:
+ message = ""
+
+ if len(items) > 1:
+ message = _(
+ "Items rate has been updated to zero as Allow Zero Valuation Rate is checked for the following items: {0}"
+ ).format(", ".join(frappe.bold(item) for item in items))
+ else:
+ message = _(
+ "Item rate has been updated to zero as Allow Zero Valuation Rate is checked for item {0}"
+ ).format(frappe.bold(items[0]))
+
+ frappe.msgprint(message, alert=True)
+
def set_rate_for_outgoing_items(self, reset_outgoing_rate=True, raise_error_if_no_rate=True):
outgoing_items_cost = 0.0
for d in self.get("items"):
diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv
index 8367afd331..bace129213 100644
--- a/erpnext/translations/fr.csv
+++ b/erpnext/translations/fr.csv
@@ -2801,7 +2801,7 @@ Stock Ledger Entries and GL Entries are reposted for the selected Purchase Recei
Stock Levels,Niveaux du Stocks,
Stock Liabilities,Passif du Stock,
Stock Options,Options du Stock,
-Stock Qty,Qté en Stock,
+Stock Qty,Qté en unité de stock,
Stock Received But Not Billed,Stock Reçus Mais Non Facturés,
Stock Reports,Rapports de stock,
Stock Summary,Résumé du Stock,