diff --git a/erpnext/accounts/doctype/account/account.json b/erpnext/accounts/doctype/account/account.json
index 7b56444e635..5f906b54aaa 100644
--- a/erpnext/accounts/doctype/account/account.json
+++ b/erpnext/accounts/doctype/account/account.json
@@ -128,7 +128,7 @@
"description": "Rate at which this tax is applied",
"fieldname": "tax_rate",
"fieldtype": "Float",
- "label": "Rate",
+ "label": "Tax Rate",
"oldfieldname": "tax_rate",
"oldfieldtype": "Currency"
},
diff --git a/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json b/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json
index 05b284ae16f..544f4fd6640 100644
--- a/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json
+++ b/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json
@@ -101,7 +101,7 @@
"fieldname": "rate",
"fieldtype": "Float",
"in_list_view": 1,
- "label": "Rate",
+ "label": "Tax Rate",
"oldfieldname": "rate",
"oldfieldtype": "Currency"
},
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
index 42b1a54dea6..ce7e9436ad5 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -12,6 +12,7 @@ from frappe.utils import cint, flt
from erpnext import get_default_cost_center
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
+from erpnext.accounts.party import get_party_account
from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
get_amounts_not_reflected_in_system,
get_entries,
@@ -304,54 +305,56 @@ def create_payment_entry_bts(
bank_transaction = frappe.db.get_values(
"Bank Transaction",
bank_transaction_name,
- fieldname=["name", "unallocated_amount", "deposit", "bank_account"],
+ fieldname=["name", "unallocated_amount", "deposit", "bank_account", "currency"],
as_dict=True,
)[0]
- paid_amount = bank_transaction.unallocated_amount
+
payment_type = "Receive" if bank_transaction.deposit > 0.0 else "Pay"
- company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
- company = frappe.get_value("Account", company_account, "company")
- payment_entry_dict = {
- "company": company,
- "payment_type": payment_type,
- "reference_no": reference_number,
- "reference_date": reference_date,
- "party_type": party_type,
- "party": party,
- "posting_date": posting_date,
- "paid_amount": paid_amount,
- "received_amount": paid_amount,
- }
- payment_entry = frappe.new_doc("Payment Entry")
+ bank_account = frappe.get_cached_value("Bank Account", bank_transaction.bank_account, "account")
+ company = frappe.get_cached_value("Account", bank_account, "company")
+ party_account = get_party_account(party_type, party, company)
- payment_entry.update(payment_entry_dict)
+ bank_currency = bank_transaction.currency
+ party_currency = frappe.get_cached_value("Account", party_account, "account_currency")
- if mode_of_payment:
- payment_entry.mode_of_payment = mode_of_payment
- if project:
- payment_entry.project = project
- if cost_center:
- payment_entry.cost_center = cost_center
- if payment_type == "Receive":
- payment_entry.paid_to = company_account
- else:
- payment_entry.paid_from = company_account
+ exc_rate = get_exchange_rate(bank_currency, party_currency, posting_date)
- payment_entry.validate()
+ amt_in_bank_acc_currency = bank_transaction.unallocated_amount
+ amount_in_party_currency = bank_transaction.unallocated_amount * exc_rate
+
+ pe = frappe.new_doc("Payment Entry")
+ pe.payment_type = payment_type
+ pe.company = company
+ pe.reference_no = reference_number
+ pe.reference_date = reference_date
+ pe.party_type = party_type
+ pe.party = party
+ pe.posting_date = posting_date
+ pe.paid_from = party_account if payment_type == "Receive" else bank_account
+ pe.paid_to = party_account if payment_type == "Pay" else bank_account
+ pe.paid_from_account_currency = party_currency if payment_type == "Receive" else bank_currency
+ pe.paid_to_account_currency = party_currency if payment_type == "Pay" else bank_currency
+ pe.paid_amount = amount_in_party_currency if payment_type == "Receive" else amt_in_bank_acc_currency
+ pe.received_amount = amount_in_party_currency if payment_type == "Pay" else amt_in_bank_acc_currency
+ pe.mode_of_payment = mode_of_payment
+ pe.project = project
+ pe.cost_center = cost_center
+
+ pe.validate()
if allow_edit:
- return payment_entry
+ return pe
- payment_entry.insert()
+ pe.insert()
+ pe.submit()
- payment_entry.submit()
vouchers = json.dumps(
[
{
"payment_doctype": "Payment Entry",
- "payment_name": payment_entry.name,
- "amount": paid_amount,
+ "payment_name": pe.name,
+ "amount": amt_in_bank_acc_currency,
}
]
)
@@ -480,8 +483,12 @@ def get_linked_payments(
def subtract_allocations(gl_account, vouchers):
"Look up & subtract any existing Bank Transaction allocations"
copied = []
+
+ voucher_docs = [(voucher.get("doctype"), voucher.get("name")) for voucher in vouchers]
+ voucher_allocated_amounts = get_total_allocated_amount(voucher_docs)
+
for voucher in vouchers:
- rows = get_total_allocated_amount(voucher.get("doctype"), voucher.get("name"))
+ rows = voucher_allocated_amounts.get((voucher.get("doctype"), voucher.get("name"))) or []
filtered_row = list(filter(lambda row: row.get("gl_account") == gl_account, rows))
if amount := None if not filtered_row else filtered_row[0]["total"]:
@@ -719,7 +726,7 @@ def get_pe_matching_query(
(ref_rank + amount_rank + party_rank + 1).as_("rank"),
ConstantColumn("Payment Entry").as_("doctype"),
pe.name,
- pe.paid_amount_after_tax.as_("paid_amount"),
+ pe.base_paid_amount_after_tax.as_("paid_amount"),
pe.reference_no,
pe.reference_date,
pe.party,
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index c13dbe445f1..7e509896fec 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -154,10 +154,16 @@ class BankTransaction(Document):
"""
remaining_amount = self.unallocated_amount
to_remove = []
+ payment_entry_docs = [(pe.payment_document, pe.payment_entry) for pe in self.payment_entries]
+ pe_bt_allocations = get_total_allocated_amount(payment_entry_docs)
+
for payment_entry in self.payment_entries:
if payment_entry.allocated_amount == 0.0:
unallocated_amount, should_clear, latest_transaction = get_clearance_details(
- self, payment_entry
+ self,
+ payment_entry,
+ pe_bt_allocations.get((payment_entry.payment_document, payment_entry.payment_entry))
+ or [],
)
if 0.0 == unallocated_amount:
@@ -232,7 +238,7 @@ def get_doctypes_for_bank_reconciliation():
return frappe.get_hooks("bank_reconciliation_doctypes")
-def get_clearance_details(transaction, payment_entry):
+def get_clearance_details(transaction, payment_entry, bt_allocations):
"""
There should only be one bank gle for a voucher.
Could be none for a Bank Transaction.
@@ -241,7 +247,6 @@ def get_clearance_details(transaction, payment_entry):
"""
gl_bank_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
gles = get_related_bank_gl_entries(payment_entry.payment_document, payment_entry.payment_entry)
- bt_allocations = get_total_allocated_amount(payment_entry.payment_document, payment_entry.payment_entry)
unallocated_amount = min(
transaction.unallocated_amount,
@@ -294,44 +299,52 @@ def get_related_bank_gl_entries(doctype, docname):
)
-def get_total_allocated_amount(doctype, docname):
+def get_total_allocated_amount(docs):
"""
Gets the sum of allocations for a voucher on each bank GL account
along with the latest bank transaction name & date
NOTE: query may also include just saved vouchers/payments but with zero allocated_amount
"""
+ if not docs:
+ return {}
+
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
result = frappe.db.sql(
"""
- SELECT total, latest_name, latest_date, gl_account FROM (
+ SELECT total, latest_name, latest_date, gl_account, payment_document, payment_entry FROM (
SELECT
ROW_NUMBER() OVER w AS rownum,
- SUM(btp.allocated_amount) OVER(PARTITION BY ba.account) AS total,
+ SUM(btp.allocated_amount) OVER(PARTITION BY ba.account, btp.payment_document, btp.payment_entry) AS total,
FIRST_VALUE(bt.name) OVER w AS latest_name,
FIRST_VALUE(bt.date) OVER w AS latest_date,
- ba.account AS gl_account
+ ba.account AS gl_account,
+ btp.payment_document,
+ btp.payment_entry
FROM
`tabBank Transaction Payments` btp
LEFT JOIN `tabBank Transaction` bt ON bt.name=btp.parent
LEFT JOIN `tabBank Account` ba ON ba.name=bt.bank_account
WHERE
- btp.payment_document = %(doctype)s
- AND btp.payment_entry = %(docname)s
+ (btp.payment_document, btp.payment_entry) IN %(docs)s
AND bt.docstatus = 1
- WINDOW w AS (PARTITION BY ba.account ORDER BY bt.date desc)
+ WINDOW w AS (PARTITION BY ba.account, btp.payment_document, btp.payment_entry ORDER BY bt.date DESC)
) temp
WHERE
rownum = 1
""",
- dict(doctype=doctype, docname=docname),
+ dict(docs=docs),
as_dict=True,
)
+
+ payment_allocation_details = {}
for row in result:
# Why is this *sometimes* a byte string?
if isinstance(row["latest_name"], bytes):
row["latest_name"] = row["latest_name"].decode()
row["latest_date"] = frappe.utils.getdate(row["latest_date"])
- return result
+ payment_allocation_details.setdefault((row["payment_document"], row["payment_entry"]), []).append(row)
+
+ return payment_allocation_details
def get_paid_amount(payment_entry, currency, gl_bank_account):
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index a7e7edb098d..d9d7807a561 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -13,7 +13,11 @@ import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
)
-from erpnext.accounts.party import validate_party_frozen_disabled, validate_party_gle_currency
+from erpnext.accounts.party import (
+ validate_account_party_type,
+ validate_party_frozen_disabled,
+ validate_party_gle_currency,
+)
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
from erpnext.exceptions import InvalidAccountCurrency
@@ -268,6 +272,7 @@ class GLEntry(Document):
def validate_party(self):
validate_party_frozen_disabled(self.party_type, self.party)
+ validate_account_party_type(self)
def validate_currency(self):
company_currency = erpnext.get_company_currency(self.company)
diff --git a/erpnext/accounts/doctype/gl_entry/test_gl_entry.py b/erpnext/accounts/doctype/gl_entry/test_gl_entry.py
index 3edfd67b005..f6ed163bff5 100644
--- a/erpnext/accounts/doctype/gl_entry/test_gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/test_gl_entry.py
@@ -79,3 +79,48 @@ class TestGLEntry(unittest.TestCase):
"SELECT current from tabSeries where name = %s", naming_series
)[0][0]
self.assertEqual(old_naming_series_current_value + 2, new_naming_series_current_value)
+
+ def test_validate_account_party_type(self):
+ jv = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ 100,
+ "_Test Cost Center - _TC",
+ save=False,
+ submit=False,
+ )
+
+ for row in jv.accounts:
+ row.party_type = "Supplier"
+ break
+
+ jv.save()
+ try:
+ jv.submit()
+ except Exception as e:
+ self.assertEqual(
+ str(e),
+ "Party Type and Party can only be set for Receivable / Payable account_Test Account Cost for Goods Sold - _TC",
+ )
+
+ jv1 = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ 100,
+ "_Test Cost Center - _TC",
+ save=False,
+ submit=False,
+ )
+
+ for row in jv.accounts:
+ row.party_type = "Customer"
+ break
+
+ jv1.save()
+ try:
+ jv1.submit()
+ except Exception as e:
+ self.assertEqual(
+ str(e),
+ "Party Type and Party can only be set for Receivable / Payable account_Test Account Cost for Goods Sold - _TC",
+ )
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 8fd5f00b583..4a45007d50a 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -27,6 +27,18 @@ frappe.ui.form.on("Payment Entry", {
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
+ // project excluded in setup_dimension_filters
+ frm.set_query("project", function (doc) {
+ let filters = {
+ company: doc.company,
+ };
+ if (doc.party_type == "Customer") filters.customer = doc.party;
+ return {
+ query: "erpnext.controllers.queries.get_project_name",
+ filters,
+ };
+ });
+
if (frm.is_new()) {
set_default_party_type(frm);
}
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index 312628d9f97..5883d4e2f1f 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -1479,7 +1479,7 @@ class TestPaymentEntry(FrappeTestCase):
parent_account="Current Liabilities - _TC",
account_name="Advances Paid",
company=company,
- account_type="Liability",
+ account_type="Payable",
)
frappe.db.set_value(
diff --git a/erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html b/erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html
index 983f49563cd..63e88cf44c2 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html
+++ b/erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html
@@ -12,15 +12,15 @@
- | {{ _('Grand Total') }} |
+ {{ _("Grand Total") }} |
{{ frappe.utils.fmt_money(data.grand_total or '', currency=currency) }} |
- | {{ _('Net Total') }} |
+ {{ _("Net Total") }} |
{{ frappe.utils.fmt_money(data.net_total or '', currency=currency) }} |
- | {{ _('Total Quantity') }} |
+ {{ _("Total Quantity") }} |
{{ data.total_quantity or '' }} |
@@ -44,7 +44,7 @@
{% for d in data.payment_reconciliation %}
- | {{ d.mode_of_payment }} |
+ {{ _(d.mode_of_payment) }} |
{{ frappe.utils.fmt_money(d.expected_amount - d.opening_amount, currency=currency) }} |
{% endfor %}
@@ -63,7 +63,7 @@
| {{ _("Account") }} |
- {{ _("Rate") }} |
+ {{ _("Tax Rate") }} |
{{ _("Amount") }} |
diff --git a/erpnext/accounts/doctype/pos_closing_entry_taxes/pos_closing_entry_taxes.json b/erpnext/accounts/doctype/pos_closing_entry_taxes/pos_closing_entry_taxes.json
index 42e7d0ef965..7e3e9c259f1 100644
--- a/erpnext/accounts/doctype/pos_closing_entry_taxes/pos_closing_entry_taxes.json
+++ b/erpnext/accounts/doctype/pos_closing_entry_taxes/pos_closing_entry_taxes.json
@@ -14,7 +14,7 @@
"fieldname": "rate",
"fieldtype": "Percent",
"in_list_view": 1,
- "label": "Rate",
+ "label": "Tax Rate",
"read_only": 1
},
{
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index be429689b5a..dd8758e68a3 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -399,6 +399,8 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
hide_fields(this.frm.doc);
if (cint(this.frm.doc.is_paid)) {
this.frm.set_value("allocate_advances_automatically", 0);
+ this.frm.set_value("payment_terms_template", "");
+ this.frm.set_value("payment_schedule", []);
if (!this.frm.doc.company) {
this.frm.set_value("is_paid", 0);
frappe.msgprint(__("Please specify Company to proceed"));
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index dc2b44e7527..cb501c1ffbc 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -507,7 +507,7 @@ class SalesInvoice(SellingController):
frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total))
def validate_pos_paid_amount(self):
- if len(self.payments) == 0 and self.is_pos:
+ if len(self.payments) == 0 and self.is_pos and flt(self.grand_total) > 0:
frappe.throw(_("At least one mode of payment is required for POS invoice."))
def check_if_consolidated_invoice(self):
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 57eb84caaa4..439fc5639e5 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -43,6 +43,7 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import (
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
+from erpnext.stock.get_item_details import get_item_tax_map
from erpnext.stock.utils import get_incoming_rate, get_stock_balance
@@ -2873,13 +2874,26 @@ class TestSalesInvoice(FrappeTestCase):
item.save()
sales_invoice = create_sales_invoice(item="T Shirt", rate=700, do_not_submit=True)
+ item_tax_map = get_item_tax_map(
+ company=sales_invoice.company,
+ item_tax_template=sales_invoice.items[0].item_tax_template,
+ )
+
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
+ self.assertEqual(sales_invoice.items[0].item_tax_rate, item_tax_map)
# Apply discount
sales_invoice.apply_discount_on = "Net Total"
sales_invoice.discount_amount = 300
sales_invoice.save()
+
+ item_tax_map = get_item_tax_map(
+ company=sales_invoice.company,
+ item_tax_template=sales_invoice.items[0].item_tax_template,
+ )
+
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
+ self.assertEqual(sales_invoice.items[0].item_tax_rate, item_tax_map)
@change_settings("Selling Settings", {"enable_discount_accounting": 1})
def test_sales_invoice_with_discount_accounting_enabled(self):
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index 5be80872db8..3033b8ad087 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -759,6 +759,17 @@ def validate_party_frozen_disabled(party_type, party_name):
frappe.msgprint(_("{0} {1} is not active").format(party_type, party_name), alert=True)
+def validate_account_party_type(self):
+ if self.party_type and self.party:
+ account_type = frappe.get_cached_value("Account", self.account, "account_type")
+ if account_type and (account_type not in ["Receivable", "Payable"]):
+ frappe.throw(
+ _(
+ "Party Type and Party can only be set for Receivable / Payable account
" "{0}"
+ ).format(self.account)
+ )
+
+
def get_dashboard_info(party_type, party, loyalty_program=None):
current_fiscal_year = get_fiscal_year(nowdate(), as_dict=True)
diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
index 604c0a6569d..f7a2e40b4ba 100644
--- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
+++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
@@ -318,7 +318,7 @@ def get_columns(additional_table_columns, filters):
"width": 100,
},
{
- "label": _("Rate"),
+ "label": _("Tax Rate"),
"fieldname": "rate",
"fieldtype": "Float",
"options": "currency",
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 144039b794f..51b4ed248ce 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -130,7 +130,9 @@ def get_fiscal_years(
else:
return ((fy.name, fy.year_start_date, fy.year_end_date),)
- error_msg = _("""{0} {1} is not in any active Fiscal Year""").format(label, formatdate(transaction_date))
+ error_msg = _("""{0} {1} is not in any active Fiscal Year""").format(
+ _(label), formatdate(transaction_date)
+ )
if company:
error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company))
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index e499b5e0faa..2e0ba2c8218 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -19,6 +19,7 @@ from frappe.utils import (
)
import erpnext
+from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
from erpnext.accounts.general_ledger import make_reverse_gl_entries
from erpnext.assets.doctype.asset.depreciation import (
get_comma_separated_links,
@@ -887,6 +888,7 @@ def get_asset_naming_series():
@frappe.whitelist()
def make_sales_invoice(asset, item_code, company, serial_no=None):
+ asset_doc = frappe.get_doc("Asset", asset)
si = frappe.new_doc("Sales Invoice")
si.company = company
si.currency = frappe.get_cached_value("Company", company, "default_currency")
@@ -903,6 +905,16 @@ def make_sales_invoice(asset, item_code, company, serial_no=None):
"qty": 1,
},
)
+
+ accounting_dimensions = get_dimensions(with_cost_center_and_project=True)
+ for dimension in accounting_dimensions[0]:
+ si.update(
+ {
+ dimension["fieldname"]: asset_doc.get(dimension["fieldname"])
+ or dimension.get("default_dimension")
+ }
+ )
+
si.set_missing_values()
return si
diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py
index 6d2034d1878..f583ce3e6c8 100644
--- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py
+++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py
@@ -18,11 +18,12 @@ def execute(filters=None):
columns = get_columns(filters)
data = get_data(filters)
- update_received_amount(data)
if not data:
return [], [], None, []
+ update_received_amount(data)
+
data, chart_data = prepare_data(data, filters)
return columns, data, None, chart_data
@@ -103,6 +104,11 @@ def get_received_amount_data(data):
pr = frappe.qb.DocType("Purchase Receipt")
pr_item = frappe.qb.DocType("Purchase Receipt Item")
+ po_items = [row.name for row in data]
+
+ if not po_items:
+ return frappe._dict()
+
query = (
frappe.qb.from_(pr)
.inner_join(pr_item)
@@ -111,12 +117,10 @@ def get_received_amount_data(data):
pr_item.purchase_order_item,
Sum(pr_item.base_amount).as_("received_qty_amount"),
)
- .where((pr_item.parent == pr.name) & (pr.docstatus == 1))
+ .where((pr.docstatus == 1) & (pr_item.purchase_order_item.isin(po_items)))
.groupby(pr_item.purchase_order_item)
)
- query = query.where(pr_item.purchase_order_item.isin([row.name for row in data]))
-
data = query.run()
if not data:
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 20b5885bfdc..f9c875477b9 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -464,9 +464,16 @@ class AccountsController(TransactionBase):
)
def validate_invoice_documents_schedule(self):
- if self.is_return:
+ if (
+ self.is_return
+ or (self.doctype == "Purchase Invoice" and self.is_paid)
+ or (self.doctype == "Sales Invoice" and self.is_pos)
+ or self.get("is_opening") == "Yes"
+ ):
self.payment_terms_template = ""
self.payment_schedule = []
+
+ if self.is_return:
return
self.validate_payment_schedule_dates()
@@ -1219,7 +1226,7 @@ class AccountsController(TransactionBase):
party_account = []
default_advance_account = None
- if self.doctype == "Sales Invoice":
+ if self.doctype in ["Sales Invoice", "POS Invoice"]:
party_type = "Customer"
party = self.customer
amount_field = "credit_in_account_currency"
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 6020dce0761..8da22785b94 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -9,6 +9,7 @@ from frappe.utils import cint, flt, getdate
from frappe.utils.data import nowtime
import erpnext
+from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
from erpnext.accounts.party import get_party_details
from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
@@ -744,6 +745,7 @@ class BuyingController(SubcontractingController):
def auto_make_assets(self, asset_items):
items_data = get_asset_item_details(asset_items)
messages = []
+ accounting_dimensions = get_dimensions(with_cost_center_and_project=True)
for d in self.items:
if d.is_fixed_asset:
@@ -755,11 +757,11 @@ class BuyingController(SubcontractingController):
if item_data.get("asset_naming_series"):
created_assets = []
if item_data.get("is_grouped_asset"):
- asset = self.make_asset(d, is_grouped_asset=True)
+ asset = self.make_asset(d, accounting_dimensions, is_grouped_asset=True)
created_assets.append(asset)
else:
for _qty in range(cint(d.qty)):
- asset = self.make_asset(d)
+ asset = self.make_asset(d, accounting_dimensions)
created_assets.append(asset)
if len(created_assets) > 5:
@@ -797,7 +799,7 @@ class BuyingController(SubcontractingController):
for message in messages:
frappe.msgprint(message, title="Success", indicator="green")
- def make_asset(self, row, is_grouped_asset=False):
+ def make_asset(self, row, accounting_dimensions, is_grouped_asset=False):
if not row.asset_location:
frappe.throw(_("Row {0}: Enter location for the asset item {1}").format(row.idx, row.item_code))
@@ -828,6 +830,13 @@ class BuyingController(SubcontractingController):
"purchase_invoice_item": row.name if self.doctype == "Purchase Invoice" else None,
}
)
+ for dimension in accounting_dimensions[0]:
+ asset.update(
+ {
+ dimension["fieldname"]: self.get(dimension["fieldname"])
+ or dimension.get("default_dimension")
+ }
+ )
asset.flags.ignore_validate = True
asset.flags.ignore_mandatory = True
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index 463cb859970..03852d3739a 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -271,10 +271,14 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
qb_filter_or_conditions = []
ifelse = CustomFunction("IF", ["condition", "then", "else"])
- if filters and filters.get("customer"):
- qb_filter_and_conditions.append(
- (proj.customer == filters.get("customer")) | proj.customer.isnull() | proj.customer == ""
- )
+ if filters:
+ if filters.get("customer"):
+ qb_filter_and_conditions.append(
+ (proj.customer == filters.get("customer")) | proj.customer.isnull() | proj.customer == ""
+ )
+
+ if filters.get("company"):
+ qb_filter_and_conditions.append(proj.company == filters.get("company"))
qb_filter_and_conditions.append(proj.status.notin(["Completed", "Cancelled"]))
diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py
index e8aa6254d34..e1cd0a1c340 100644
--- a/erpnext/controllers/status_updater.py
+++ b/erpnext/controllers/status_updater.py
@@ -126,9 +126,13 @@ status_map = {
"Partially Received",
"eval:self.status != 'Stopped' and self.per_received > 0 and self.per_received < 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'",
],
+ [
+ "Partially Received",
+ "eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1 and self.material_request_type == 'Material Transfer'",
+ ],
[
"Partially Ordered",
- "eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1",
+ "eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1 and self.material_request_type != 'Material Transfer'",
],
[
"Manufactured",
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index bf5beab1a82..8fecb177295 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -18,7 +18,7 @@ from erpnext.controllers.accounts_controller import (
validate_inclusive_tax,
validate_taxes_and_charges,
)
-from erpnext.stock.get_item_details import _get_item_tax_template
+from erpnext.stock.get_item_details import _get_item_tax_template, get_item_tax_map
from erpnext.utilities.regional import temporary_flag
@@ -70,6 +70,7 @@ class calculate_taxes_and_totals:
self.validate_conversion_rate()
self.calculate_item_values()
self.validate_item_tax_template()
+ self.update_item_tax_map()
self.initialize_taxes()
self.determine_exclusive_rate()
self.calculate_net_total()
@@ -134,6 +135,14 @@ class calculate_taxes_and_totals:
)
)
+ def update_item_tax_map(self):
+ for item in self.doc.items:
+ item.item_tax_rate = get_item_tax_map(
+ company=self.doc.get("company"),
+ item_tax_template=item.item_tax_template,
+ as_json=True,
+ )
+
def validate_conversion_rate(self):
# validate conversion rate
company_currency = erpnext.get_company_currency(self.doc.company)
diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.json b/erpnext/manufacturing/doctype/blanket_order/blanket_order.json
index a63fc4da69a..0e2e7273f10 100644
--- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.json
+++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.json
@@ -13,6 +13,8 @@
"supplier",
"supplier_name",
"column_break_8",
+ "order_no",
+ "order_date",
"from_date",
"to_date",
"company",
@@ -129,15 +131,27 @@
"fieldname": "terms",
"fieldtype": "Text Editor",
"label": "Terms and Conditions Details"
+ },
+ {
+ "fieldname": "order_no",
+ "fieldtype": "Data",
+ "label": "Order No"
+ },
+ {
+ "depends_on": "eval:doc.order_no",
+ "fieldname": "order_date",
+ "fieldtype": "Date",
+ "label": "Order Date"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-06-29 00:30:30.621636",
+ "modified": "2024-12-05 15:44:21.520093",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Blanket Order",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py
index 5ecf9d3a9d6..26fce935abb 100644
--- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py
+++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py
@@ -31,6 +31,8 @@ class BlanketOrder(Document):
from_date: DF.Date
items: DF.Table[BlanketOrderItem]
naming_series: DF.Literal["MFG-BLR-.YYYY.-"]
+ order_date: DF.Date | None
+ order_no: DF.Data | None
supplier: DF.Link | None
supplier_name: DF.Data | None
tc_name: DF.Link | None
diff --git a/erpnext/manufacturing/doctype/bom/bom_list.js b/erpnext/manufacturing/doctype/bom/bom_list.js
index a26df545f85..1b6cba90dbc 100644
--- a/erpnext/manufacturing/doctype/bom/bom_list.js
+++ b/erpnext/manufacturing/doctype/bom/bom_list.js
@@ -2,13 +2,13 @@ frappe.listview_settings["BOM"] = {
add_fields: ["is_active", "is_default", "total_cost", "has_variants"],
get_indicator: function (doc) {
if (doc.is_active && doc.has_variants) {
- return [__("Template"), "orange", "has_variants,=,Yes"];
+ return [__("Template"), "orange", "has_variants,=,1"];
} else if (doc.is_default) {
- return [__("Default"), "green", "is_default,=,Yes"];
+ return [__("Default"), "green", "is_default,=,1"];
} else if (doc.is_active) {
- return [__("Active"), "blue", "is_active,=,Yes"];
+ return [__("Active"), "blue", "is_active,=,1"];
} else if (!doc.is_active) {
- return [__("Not active"), "gray", "is_active,=,No"];
+ return [__("Not active"), "gray", "is_active,=,0"];
}
},
};
diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py
index 0f5fa959dc5..6e66c9e539d 100644
--- a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py
+++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py
@@ -230,8 +230,8 @@ class ForecastingReport(ExponentialSmoothingForecast):
"data": {
"labels": labels,
"datasets": [
- {"name": "Demand", "values": self.total_demand},
- {"name": "Forecast", "values": self.total_forecast},
+ {"name": _("Demand"), "values": self.total_demand},
+ {"name": _("Forecast"), "values": self.total_forecast},
],
},
"type": "line",
diff --git a/erpnext/manufacturing/report/production_analytics/production_analytics.py b/erpnext/manufacturing/report/production_analytics/production_analytics.py
index dc2b9ad62f3..e511612d3a3 100644
--- a/erpnext/manufacturing/report/production_analytics/production_analytics.py
+++ b/erpnext/manufacturing/report/production_analytics/production_analytics.py
@@ -106,7 +106,7 @@ def get_data(filters, columns):
for label in labels:
work = {}
- work["Status"] = label
+ work["Status"] = _(label)
for _dummy, end_date in ranges:
period = get_period(end_date, filters)
if periodic_data.get(label).get(period):
diff --git a/erpnext/manufacturing/report/test_reports.py b/erpnext/manufacturing/report/test_reports.py
index 3e20f310ff9..f9c3154a63b 100644
--- a/erpnext/manufacturing/report/test_reports.py
+++ b/erpnext/manufacturing/report/test_reports.py
@@ -4,61 +4,67 @@ import frappe
from erpnext.tests.utils import ReportFilters, ReportName, execute_script_report
-DEFAULT_FILTERS = {
- "company": "_Test Company",
- "from_date": "2010-01-01",
- "to_date": "2030-01-01",
- "warehouse": "_Test Warehouse - _TC",
-}
-
-
-REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
- ("BOM Explorer", {"bom": frappe.get_last_doc("BOM").name}),
- ("BOM Operations Time", {}),
- ("BOM Stock Calculated", {"bom": frappe.get_last_doc("BOM").name, "qty_to_make": 2}),
- ("BOM Stock Report", {"bom": frappe.get_last_doc("BOM").name, "qty_to_produce": 2}),
- ("Cost of Poor Quality Report", {"item": "_Test Item", "serial_no": "00"}),
- ("Downtime Analysis", {}),
- (
- "Exponential Smoothing Forecasting",
- {
- "based_on_document": "Sales Order",
- "based_on_field": "Qty",
- "no_of_years": 3,
- "periodicity": "Yearly",
- "smoothing_constant": 0.3,
- },
- ),
- ("Job Card Summary", {"fiscal_year": "2021-2022"}),
- ("Production Analytics", {"range": "Monthly"}),
- ("Quality Inspection Summary", {}),
- ("Process Loss Report", {}),
- ("Work Order Stock Report", {}),
- ("Work Order Summary", {"fiscal_year": "2021-2022", "age": 0}),
-]
-
-
-if frappe.db.a_row_exists("Production Plan"):
- REPORT_FILTER_TEST_CASES.append(
- ("Production Plan Summary", {"production_plan": frappe.get_last_doc("Production Plan").name})
- )
-
-OPTIONAL_FILTERS = {
- "warehouse": "_Test Warehouse - _TC",
- "item": "_Test Item",
- "item_group": "_Test Item Group",
-}
-
class TestManufacturingReports(unittest.TestCase):
+ def setUp(self):
+ self.setup_default_filters()
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+ def setup_default_filters(self):
+ self.last_bom = frappe.get_last_doc("BOM").name
+ self.DEFAULT_FILTERS = {
+ "company": "_Test Company",
+ "from_date": "2010-01-01",
+ "to_date": "2030-01-01",
+ "warehouse": "_Test Warehouse - _TC",
+ }
+
+ self.REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
+ ("BOM Explorer", {"bom": self.last_bom}),
+ ("BOM Operations Time", {}),
+ ("BOM Stock Calculated", {"bom": self.last_bom, "qty_to_make": 2}),
+ ("BOM Stock Report", {"bom": self.last_bom, "qty_to_produce": 2}),
+ ("Cost of Poor Quality Report", {"item": "_Test Item", "serial_no": "00"}),
+ ("Downtime Analysis", {}),
+ (
+ "Exponential Smoothing Forecasting",
+ {
+ "based_on_document": "Sales Order",
+ "based_on_field": "Qty",
+ "no_of_years": 3,
+ "periodicity": "Yearly",
+ "smoothing_constant": 0.3,
+ },
+ ),
+ ("Job Card Summary", {"fiscal_year": "2021-2022"}),
+ ("Production Analytics", {"range": "Monthly"}),
+ ("Quality Inspection Summary", {}),
+ ("Process Loss Report", {}),
+ ("Work Order Stock Report", {}),
+ ("Work Order Summary", {"fiscal_year": "2021-2022", "age": 0}),
+ ]
+
+ if frappe.db.a_row_exists("Production Plan"):
+ self.REPORT_FILTER_TEST_CASES.append(
+ ("Production Plan Summary", {"production_plan": frappe.get_last_doc("Production Plan").name})
+ )
+
+ self.OPTIONAL_FILTERS = {
+ "warehouse": "_Test Warehouse - _TC",
+ "item": "_Test Item",
+ "item_group": "_Test Item Group",
+ }
+
def test_execute_all_manufacturing_reports(self):
"""Test that all script report in manufacturing modules are executable with supported filters"""
- for report, filter in REPORT_FILTER_TEST_CASES:
+ for report, filter in self.REPORT_FILTER_TEST_CASES:
with self.subTest(report=report):
execute_script_report(
report_name=report,
module="Manufacturing",
filters=filter,
- default_filters=DEFAULT_FILTERS,
- optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
+ default_filters=self.DEFAULT_FILTERS,
+ optional_filters=self.OPTIONAL_FILTERS if filter.get("_optional") else None,
)
diff --git a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js
index 21aa70fe7ae..b1019f67ca9 100644
--- a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js
+++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js
@@ -147,10 +147,10 @@ class BOMConfigurator {
if (!node.expanded) {
view.tree.load_children(node, true);
$(node.parent[0]).find(".tree-children").show();
- node.$toolbar.find(".expand-all-btn").html("Collapse All");
+ node.$toolbar.find(".expand-all-btn").html(__("Collapse All"));
} else {
node.$tree_link.trigger("click");
- node.$toolbar.find(".expand-all-btn").html("Expand All");
+ node.$toolbar.find(".expand-all-btn").html(__("Expand All"));
}
},
condition: function (node) {
@@ -190,10 +190,10 @@ class BOMConfigurator {
if (!node.expanded) {
view.tree.load_children(node, true);
$(node.parent[0]).find(".tree-children").show();
- node.$toolbar.find(".expand-all-btn").html("Collapse All");
+ node.$toolbar.find(".expand-all-btn").html(__("Collapse All"));
} else {
node.$tree_link.trigger("click");
- node.$toolbar.find(".expand-all-btn").html("Expand All");
+ node.$toolbar.find(".expand-all-btn").html(__("Expand All"));
}
},
condition: function (node) {
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index 202efe157f0..af61d5f0258 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -25,6 +25,14 @@ erpnext.buying = {
};
});
+ this.frm.set_query("project", function (doc) {
+ return {
+ filters: {
+ company: doc.company,
+ },
+ };
+ });
+
if (this.frm.doc.__islocal
&& frappe.meta.has_field(this.frm.doc.doctype, "disable_rounded_total")) {
@@ -145,6 +153,18 @@ erpnext.buying = {
});
}
+ company(){
+ if(!frappe.meta.has_field(this.frm.doc.doctype, "billing_address")) return;
+
+ frappe.call({
+ method: "erpnext.setup.doctype.company.company.get_default_company_address",
+ args: { name: this.frm.doc.company, existing_address:this.frm.doc.billing_address },
+ callback: (r) => {
+ this.frm.set_value("billing_address", r.message || "");
+ },
+ });
+ }
+
supplier_address() {
erpnext.utils.get_address_display(this.frm);
erpnext.utils.set_taxes_from_address(this.frm, "supplier_address", "supplier_address", "supplier_address");
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 18cddd7f7a1..db866bd3b76 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -813,6 +813,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
validate() {
+ this.apply_pricing_rule()
this.calculate_taxes_and_totals(false);
}
@@ -959,7 +960,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
let is_drop_ship = me.frm.doc.items.some(item => item.delivered_by_supplier);
if (!is_drop_ship) {
- console.log('get_shipping_address');
erpnext.utils.get_shipping_address(this.frm, function() {
set_party_account(set_pricing);
});
@@ -975,6 +975,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
transaction_date() {
+ this.apply_pricing_rule()
if (this.frm.doc.transaction_date) {
this.frm.transaction_date = this.frm.doc.transaction_date;
frappe.ui.form.trigger(this.frm.doc.doctype, "currency");
@@ -983,6 +984,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
posting_date() {
var me = this;
+ me.apply_pricing_rule()
if (this.frm.doc.posting_date) {
this.frm.posting_date = this.frm.doc.posting_date;
@@ -2310,6 +2312,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
fieldname: "batch_no",
label: __("Batch No"),
hidden: true
+ },
+ {
+ fieldtype: "Data",
+ fieldname: "child_row_reference",
+ label: __("Child Row Reference"),
+ hidden: true
}
]
}
@@ -2353,14 +2361,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if (this.has_inspection_required(item)) {
let dialog_items = dialog.fields_dict.items;
dialog_items.df.data.push({
- "docname": item.name,
"item_code": item.item_code,
"item_name": item.item_name,
"qty": item.qty,
"description": item.description,
"serial_no": item.serial_no,
"batch_no": item.batch_no,
- "sample_size": item.sample_quantity
+ "sample_size": item.sample_quantity,
+ "child_row_reference": item.name,
});
dialog_items.grid.refresh();
}
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index 32cb2bc0525..dc7e992f654 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -285,6 +285,7 @@ erpnext.PointOfSale.Controller = class {
edit_cart: () => this.payment.edit_cart(),
customer_details_updated: (details) => {
+ this.item_selector.load_items_data();
this.customer_details = details;
// will add/remove LP payment method
this.payment.render_loyalty_points_payment_mode();
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 6342b237f6e..325f7b258a9 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_cart.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -390,6 +390,14 @@ erpnext.PointOfSale.ItemCart = class {
input_class: "input-xs",
onchange: function () {
this.value = flt(this.value);
+ if (this.value > 100) {
+ frappe.msgprint({
+ title: __("Invalid Discount"),
+ indicator: "red",
+ message: __("Discount cannot be greater than 100%."),
+ });
+ this.value = 0;
+ }
frappe.model.set_value(
frm.doc.doctype,
frm.doc.name,
diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js
index ad4b4cd15be..2c93a0d546b 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_details.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_details.js
@@ -315,8 +315,12 @@ erpnext.PointOfSale.ItemDetails = class {
frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => {
const field_control = this[`${fieldname}_control`];
const item_row_is_being_edited = this.compare_with_current_item(item_row);
-
- if (item_row_is_being_edited && field_control && field_control.get_value() !== value) {
+ if (
+ item_row_is_being_edited &&
+ field_control &&
+ field_control.get_value() !== value &&
+ value == item_row[fieldname]
+ ) {
field_control.set_value(value);
cur_pos.update_cart_html(item_row);
}
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js
index bea1918fa20..92349d27aca 100644
--- a/erpnext/selling/page/point_of_sale/pos_payment.js
+++ b/erpnext/selling/page/point_of_sale/pos_payment.js
@@ -235,7 +235,7 @@ erpnext.PointOfSale.Payment = class {
frappe.ui.form.on("Sales Invoice Payment", "amount", (frm, cdt, cdn) => {
// for setting correct amount after loyalty points are redeemed
const default_mop = locals[cdt][cdn];
- const mode = default_mop.mode_of_payment.replace(/ +/g, "_").toLowerCase();
+ const mode = this.sanitize_mode_of_payment(default_mop.mode_of_payment);
if (this[`${mode}_control`] && this[`${mode}_control`].get_value() != default_mop.amount) {
this[`${mode}_control`].set_value(default_mop.amount);
}
@@ -388,7 +388,7 @@ erpnext.PointOfSale.Payment = class {
this.$payment_modes.html(
`${payments
.map((p, i) => {
- const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase();
+ const mode = this.sanitize_mode_of_payment(p.mode_of_payment);
const payment_type = p.type;
const margin = i % 2 === 0 ? "pr-2" : "pl-2";
const amount = p.amount > 0 ? format_currency(p.amount, currency) : "";
@@ -407,7 +407,7 @@ erpnext.PointOfSale.Payment = class {
);
payments.forEach((p) => {
- const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase();
+ const mode = this.sanitize_mode_of_payment(p.mode_of_payment);
const me = this;
this[`${mode}_control`] = frappe.ui.form.make_control({
df: {
@@ -442,7 +442,7 @@ erpnext.PointOfSale.Payment = class {
const doc = this.events.get_frm().doc;
const payments = doc.payments;
payments.forEach((p) => {
- const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase();
+ const mode = this.sanitize_mode_of_payment(p.mode_of_payment);
if (p.default) {
setTimeout(() => {
this.$payment_modes.find(`.${mode}.mode-of-payment-control`).parent().click();
@@ -612,4 +612,12 @@ erpnext.PointOfSale.Payment = class {
toggle_component(show) {
show ? this.$component.css("display", "flex") : this.$component.css("display", "none");
}
+
+ sanitize_mode_of_payment(mode_of_payment) {
+ return mode_of_payment
+ .replace(/ +/g, "_")
+ .replace(/[^\p{L}\p{N}_-]/gu, "")
+ .replace(/^[^_a-zA-Z\p{L}]+/u, "")
+ .toLowerCase();
+ }
};
diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py
index 6561f386c55..f8cd61d50ea 100644
--- a/erpnext/setup/setup_wizard/operations/taxes_setup.py
+++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py
@@ -292,7 +292,7 @@ def get_or_create_tax_group(company_name, root_type):
tax_group_account.flags.ignore_links = True
tax_group_account.flags.ignore_validate = True
- tax_group_account.insert(ignore_permissions=True)
+ tax_group_account.insert(ignore_permissions=True, ignore_if_duplicate=True)
tax_group_name = tax_group_account.name
diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py
index b3f4c2dead4..7611d751fdd 100644
--- a/erpnext/stock/deprecated_serial_batch.py
+++ b/erpnext/stock/deprecated_serial_batch.py
@@ -181,6 +181,9 @@ class DeprecatedBatchNoValuation:
stock_value_change = self.batch_avg_rate[batch_no] * ledger.qty
self.stock_value_change += stock_value_change
+ self.non_batchwise_balance_value[batch_no] -= stock_value_change
+ self.non_batchwise_balance_qty[batch_no] -= ledger.qty
+
frappe.db.set_value(
"Serial and Batch Entry",
ledger.name,
@@ -220,7 +223,6 @@ class DeprecatedBatchNoValuation:
.select(
sle.batch_no,
Sum(sle.actual_qty).as_("batch_qty"),
- Sum(sle.stock_value_difference).as_("batch_value"),
)
.where(
(sle.item_code == self.sle.item_code)
@@ -237,11 +239,59 @@ class DeprecatedBatchNoValuation:
if self.sle.name:
query = query.where(sle.name != self.sle.name)
- for d in query.run(as_dict=True):
- self.non_batchwise_balance_value[d.batch_no] += flt(d.batch_value)
- self.non_batchwise_balance_qty[d.batch_no] += flt(d.batch_qty)
+ batch_data = query.run(as_dict=True)
+ for d in batch_data:
self.available_qty[d.batch_no] += flt(d.batch_qty)
+ last_sle = self.get_last_sle_for_non_batch()
+ for d in batch_data:
+ self.non_batchwise_balance_value[d.batch_no] += flt(last_sle.stock_value)
+ self.non_batchwise_balance_qty[d.batch_no] += flt(last_sle.qty_after_transaction)
+
+ def get_last_sle_for_non_batch(self):
+ from erpnext.stock.utils import get_combine_datetime
+
+ sle = frappe.qb.DocType("Stock Ledger Entry")
+ batch = frappe.qb.DocType("Batch")
+
+ posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
+ if not self.sle.creation:
+ posting_datetime = posting_datetime + datetime.timedelta(milliseconds=1)
+
+ timestamp_condition = sle.posting_datetime < posting_datetime
+
+ if self.sle.creation:
+ timestamp_condition |= (sle.posting_datetime == posting_datetime) & (
+ sle.creation < self.sle.creation
+ )
+
+ query = (
+ frappe.qb.from_(sle)
+ .inner_join(batch)
+ .on(sle.batch_no == batch.name)
+ .select(
+ sle.stock_value,
+ sle.qty_after_transaction,
+ )
+ .where(
+ (sle.item_code == self.sle.item_code)
+ & (sle.warehouse == self.sle.warehouse)
+ & (sle.batch_no.isnotnull())
+ & (batch.use_batchwise_valuation == 0)
+ & (sle.is_cancelled == 0)
+ )
+ .where(timestamp_condition)
+ .orderby(sle.posting_datetime, order=Order.desc)
+ .orderby(sle.creation, order=Order.desc)
+ .limit(1)
+ )
+
+ if self.sle.name:
+ query = query.where(sle.name != self.sle.name)
+
+ data = query.run(as_dict=True)
+ return data[0] if data else {}
+
@deprecated
def set_balance_value_from_bundle(self) -> None:
bundle = frappe.qb.DocType("Serial and Batch Bundle")
diff --git a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py
index 8aa49f7cfd8..f6510c04fe9 100644
--- a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py
+++ b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py
@@ -65,7 +65,7 @@ class ClosingStockBalance(Document):
& (
(table.from_date.between(self.from_date, self.to_date))
| (table.to_date.between(self.from_date, self.to_date))
- | ((table.from_date >= self.from_date) & (table.to_date >= self.to_date))
+ | ((self.from_date >= table.from_date) & (table.from_date >= self.to_date))
)
)
)
diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py
index 661605bdf5f..524c7331bc7 100644
--- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py
+++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py
@@ -237,8 +237,13 @@ class InventoryDimension(Document):
custom_fields["Stock Ledger Entry"] = dimension_field
filter_custom_fields = {}
+ ignore_doctypes = ["Serial and Batch Bundle", "Serial and Batch Entry", "Pick List Item"]
+
if custom_fields:
for doctype, fields in custom_fields.items():
+ if doctype in ignore_doctypes:
+ continue
+
if isinstance(fields, dict):
fields = [fields]
diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js
index 09c01a0ee88..83b63e225b3 100644
--- a/erpnext/stock/doctype/material_request/material_request.js
+++ b/erpnext/stock/doctype/material_request/material_request.js
@@ -322,7 +322,7 @@ frappe.ui.form.on("Material Request", {
default: 1,
},
],
- primary_action_label: "Get Items",
+ primary_action_label: __("Get Items"),
primary_action(values) {
if (!values) return;
values["company"] = frm.doc.company;
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index 23d289170db..f59b60a3f51 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -766,6 +766,7 @@ def raise_work_orders(material_request):
)
wo_order.set_work_order_operations()
+ wo_order.flags.ignore_validate = True
wo_order.flags.ignore_mandatory = True
wo_order.save()
diff --git a/erpnext/stock/doctype/material_request/material_request_list.js b/erpnext/stock/doctype/material_request/material_request_list.js
index ee5c9e7b86c..57332aa7730 100644
--- a/erpnext/stock/doctype/material_request/material_request_list.js
+++ b/erpnext/stock/doctype/material_request/material_request_list.js
@@ -14,6 +14,12 @@ frappe.listview_settings["Material Request"] = {
}
} else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) == 0) {
return [__("Pending"), "orange", "per_ordered,=,0"];
+ } else if (
+ doc.docstatus == 1 &&
+ flt(doc.per_ordered, precision) < 100 &&
+ doc.material_request_type == "Material Transfer"
+ ) {
+ return [__("Partially Received"), "yellow", "per_ordered,<,100"];
} else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) < 100) {
return [__("Partially ordered"), "yellow", "per_ordered,<,100"];
} else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) == 100) {
diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py
index 4d48d50c9f0..b0ea7964117 100644
--- a/erpnext/stock/doctype/material_request/test_material_request.py
+++ b/erpnext/stock/doctype/material_request/test_material_request.py
@@ -17,6 +17,7 @@ from erpnext.stock.doctype.material_request.material_request import (
make_supplier_quotation,
raise_work_orders,
)
+from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
class TestMaterialRequest(FrappeTestCase):
@@ -59,6 +60,43 @@ class TestMaterialRequest(FrappeTestCase):
self.assertEqual(se.doctype, "Stock Entry")
self.assertEqual(len(se.get("items")), len(mr.get("items")))
+ def test_partial_make_stock_entry(self):
+ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry as _make_stock_entry
+
+ mr = frappe.copy_doc(test_records[0]).insert()
+
+ source_wh = create_warehouse(
+ warehouse_name="_Test Source Warehouse",
+ properties={"parent_warehouse": "All Warehouses - _TC"},
+ company="_Test Company",
+ )
+
+ mr = frappe.get_doc("Material Request", mr.name)
+ mr.material_request_type = "Material Transfer"
+
+ for row in mr.items:
+ _make_stock_entry(
+ item_code=row.item_code,
+ qty=10,
+ to_warehouse=source_wh,
+ company="_Test Company",
+ rate=100,
+ )
+
+ row.from_warehouse = source_wh
+ row.qty = 10
+
+ mr.save()
+ mr.submit()
+
+ se = make_stock_entry(mr.name)
+ se.get("items")[0].qty = 5
+ se.insert()
+ se.submit()
+
+ mr.reload()
+ self.assertEqual(mr.status, "Partially Received")
+
def test_in_transit_make_stock_entry(self):
mr = frappe.copy_doc(test_records[0]).insert()
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json
index 914a9f3c21f..56017c4fee2 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json
@@ -15,6 +15,7 @@
"inspection_type",
"reference_type",
"reference_name",
+ "child_row_reference",
"section_break_7",
"item_code",
"item_serial_no",
@@ -238,6 +239,15 @@
"fieldname": "manual_inspection",
"fieldtype": "Check",
"label": "Manual Inspection"
+ },
+ {
+ "fieldname": "child_row_reference",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Child Row Reference",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
}
],
"icon": "fa fa-search",
@@ -245,7 +255,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-08-23 11:56:50.282878",
+ "modified": "2024-12-30 19:08:16.611192",
"modified_by": "Administrator",
"module": "Stock",
"name": "Quality Inspection",
@@ -272,4 +282,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
index 6890256dc04..d3b5e65ea9f 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
@@ -29,6 +29,7 @@ class QualityInspection(Document):
amended_from: DF.Link | None
batch_no: DF.Link | None
bom_no: DF.Link | None
+ child_row_reference: DF.Data | None
description: DF.SmallText | None
inspected_by: DF.Link
inspection_type: DF.Literal["", "Incoming", "Outgoing", "In Process"]
@@ -74,6 +75,64 @@ class QualityInspection(Document):
self.inspect_and_set_status()
self.validate_inspection_required()
+ self.set_child_row_reference()
+
+ def set_child_row_reference(self):
+ if self.child_row_reference:
+ return
+
+ if not (self.reference_type and self.reference_name):
+ return
+
+ doctype = self.reference_type + " Item"
+ if self.reference_type == "Stock Entry":
+ doctype = "Stock Entry Detail"
+
+ child_row_references = frappe.get_all(
+ doctype,
+ filters={"parent": self.reference_name, "item_code": self.item_code},
+ pluck="name",
+ )
+
+ if not child_row_references:
+ return
+
+ if len(child_row_references) == 1:
+ self.child_row_reference = child_row_references[0]
+ else:
+ self.distribute_child_row_reference(child_row_references)
+
+ def distribute_child_row_reference(self, child_row_references):
+ quality_inspections = frappe.get_all(
+ "Quality Inspection",
+ filters={
+ "reference_name": self.reference_name,
+ "item_code": self.item_code,
+ "docstatus": ("<", 2),
+ },
+ fields=["name", "child_row_reference", "docstatus"],
+ order_by="child_row_reference desc",
+ )
+
+ for row in quality_inspections:
+ if not child_row_references:
+ break
+
+ if row.child_row_reference and row.child_row_reference in child_row_references:
+ child_row_references.remove(row.child_row_reference)
+ continue
+
+ if row.docstatus == 1:
+ continue
+
+ if row.name == self.name:
+ self.child_row_reference = child_row_references[0]
+ else:
+ frappe.db.set_value(
+ "Quality Inspection", row.name, "child_row_reference", child_row_references[0]
+ )
+
+ child_row_references.remove(child_row_references[0])
def validate_inspection_required(self):
if self.reference_type in ["Purchase Receipt", "Purchase Invoice"] and not frappe.get_cached_value(
@@ -157,35 +216,38 @@ class QualityInspection(Document):
)
else:
- args = [quality_inspection, self.modified, self.reference_name, self.item_code]
doctype = self.reference_type + " Item"
if self.reference_type == "Stock Entry":
doctype = "Stock Entry Detail"
- if self.reference_type and self.reference_name:
- conditions = ""
+ if doctype and self.reference_name:
+ child_doc = frappe.qb.DocType(doctype)
+
+ query = (
+ frappe.qb.update(child_doc)
+ .set(child_doc.quality_inspection, quality_inspection)
+ .where(
+ (child_doc.parent == self.reference_name) & (child_doc.item_code == self.item_code)
+ )
+ )
+
if self.batch_no and self.docstatus == 1:
- conditions += " and t1.batch_no = %s"
- args.append(self.batch_no)
+ query = query.where(child_doc.batch_no == self.batch_no)
if self.docstatus == 2: # if cancel, then remove qi link wherever same name
- conditions += " and t1.quality_inspection = %s"
- args.append(self.name)
+ query = query.where(child_doc.quality_inspection == self.name)
- frappe.db.sql(
- f"""
- UPDATE
- `tab{doctype}` t1, `tab{self.reference_type}` t2
- SET
- t1.quality_inspection = %s, t2.modified = %s
- WHERE
- t1.parent = %s
- and t1.item_code = %s
- and t1.parent = t2.name
- {conditions}
- """,
- args,
+ if self.child_row_reference:
+ query = query.where(child_doc.name == self.child_row_reference)
+
+ query.run()
+
+ frappe.db.set_value(
+ self.reference_type,
+ self.reference_name,
+ "modified",
+ self.modified,
)
def inspect_and_set_status(self):
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
index 647082baa68..45a474df2b7 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
@@ -166,7 +166,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
for qty, valuation in {10: 100, 20: 200}.items():
stock_queue.append([qty, valuation])
qty_after_transaction += qty
- balance_value += qty_after_transaction * valuation
+ balance_value += qty * valuation
doc = frappe.get_doc(
{
@@ -177,6 +177,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
"incoming_rate": valuation,
"qty_after_transaction": qty_after_transaction,
"stock_value_difference": valuation * qty,
+ "stock_value": balance_value,
"balance_value": balance_value,
"valuation_rate": balance_value / qty_after_transaction,
"actual_qty": qty,
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index febc814b978..78f69b1ce62 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -3289,8 +3289,10 @@ def create_serial_and_batch_bundle(parent_doc, row, child, type_of_transaction=N
doc.append("entries", {"serial_no": serial_no, "warehouse": row.warehouse, "qty": -1})
elif row.batches_to_be_consume:
+ precision = frappe.get_precision("Serial and Batch Entry", "qty")
doc.has_batch_no = 1
for batch_no, qty in row.batches_to_be_consume.items():
+ qty = flt(qty, precision)
doc.append("entries", {"batch_no": batch_no, "warehouse": row.warehouse, "qty": qty * -1})
if not doc.entries:
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index d5d492a2c93..17a8fe2cb6a 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -592,7 +592,10 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False):
if tax.valid_from or tax.maximum_net_rate:
# In purchase Invoice first preference will be given to supplier invoice date
# if supplier date is not present then posting date
- validation_date = args.get("bill_date") or args.get("transaction_date")
+
+ validation_date = (
+ args.get("bill_date") or args.get("posting_date") or args.get("transaction_date")
+ )
if getdate(tax.valid_from) <= getdate(validation_date) and is_within_valid_range(args, tax):
taxes_with_validity.append(tax)
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
index 37cd43ac1f6..300f7a774eb 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
@@ -775,6 +775,9 @@ def make_purchase_receipt(source_name, target_doc=None, save=False, submit=False
postprocess=post_process,
)
+ if not target_doc.get("items"):
+ add_po_items_to_pr(source_doc, target_doc)
+
if (save or submit) and frappe.has_permission(target_doc.doctype, "create"):
target_doc.save()
@@ -794,3 +797,29 @@ def make_purchase_receipt(source_name, target_doc=None, save=False, submit=False
)
return target_doc
+
+
+def add_po_items_to_pr(scr_doc, target_doc):
+ fg_items = {(item.item_code, item.purchase_order): item.qty for item in scr_doc.items}
+
+ for (item_code, po_name), fg_qty in fg_items.items():
+ po_doc = frappe.get_doc("Purchase Order", po_name)
+ for item in po_doc.items:
+ if item.fg_item != item_code:
+ continue
+
+ qty = (item.stock_qty - item.received_qty) * fg_qty / item.fg_item_qty
+ if qty:
+ target_doc.append(
+ "items",
+ {
+ "item_code": item.item_code,
+ "item_name": item.item_name,
+ "description": item.description,
+ "qty": qty,
+ "rate": item.rate,
+ "warehouse": item.warehouse,
+ "purchase_order": item.parent,
+ "purchase_order_item": item.name,
+ },
+ )
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
index e0fa7923ef9..b5d190f7736 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
@@ -1137,6 +1137,80 @@ class TestSubcontractingReceipt(FrappeTestCase):
self.assertEqual(pr_details[0]["total_taxes_and_charges"], 60)
+ @change_settings("Buying Settings", {"auto_create_purchase_receipt": 1})
+ def test_auto_create_purchase_receipt_with_no_reference_of_po_item(self):
+ from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
+
+ fg_item = "Subcontracted Item SA1"
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 1",
+ "qty": 10,
+ "rate": 100,
+ "fg_item": fg_item,
+ "fg_item_qty": 5,
+ },
+ ]
+
+ po = create_purchase_order(
+ rm_items=service_items,
+ is_subcontracted=1,
+ supplier_warehouse="_Test Warehouse 1 - _TC",
+ do_not_submit=True,
+ )
+ po.append(
+ "taxes",
+ {
+ "account_head": "_Test Account Excise Duty - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Excise Duty",
+ "doctype": "Purchase Taxes and Charges",
+ "rate": 10,
+ },
+ )
+ po.save()
+ po.submit()
+
+ sco = get_subcontracting_order(po_name=po.name)
+ for row in sco.items:
+ row.db_set("purchase_order_item", None)
+
+ sco.reload()
+
+ for row in sco.items:
+ self.assertFalse(row.purchase_order_item)
+
+ rm_items = get_rm_items(sco.supplied_items)
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+ make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+
+ scr = make_subcontracting_receipt(sco.name)
+ for row in scr.items:
+ self.assertFalse(row.purchase_order_item)
+
+ scr.items[0].qty = 3
+ scr.save()
+ scr.submit()
+
+ pr_details = frappe.get_all(
+ "Purchase Receipt",
+ filters={"subcontracting_receipt": scr.name},
+ fields=["name", "total_taxes_and_charges"],
+ )
+
+ self.assertTrue(pr_details)
+
+ pr_qty = frappe.db.get_value("Purchase Receipt Item", {"parent": pr_details[0]["name"]}, "qty")
+ self.assertEqual(pr_qty, 6)
+
+ self.assertEqual(pr_details[0]["total_taxes_and_charges"], 60)
+
def test_use_serial_batch_fields_for_subcontracting_receipt(self):
fg_item = make_item(
"Test Subcontracted Item With Batch No",
diff --git a/erpnext/templates/includes/footer/footer_extension.html b/erpnext/templates/includes/footer/footer_extension.html
index 0072dc280c7..11e0adaa2ee 100644
--- a/erpnext/templates/includes/footer/footer_extension.html
+++ b/erpnext/templates/includes/footer/footer_extension.html
@@ -17,7 +17,7 @@ frappe.ready(function() {
if($("#footer-subscribe-email").val() && validate_email($("#footer-subscribe-email").val())) {
$("#footer-subscribe-email").attr('disabled', true);
- $("#footer-subscribe-button").html("Sending...")
+ $("#footer-subscribe-button").html(__("Sending..."))
.attr("disabled", true);
erpnext.subscribe_to_newsletter({
email: $("#footer-subscribe-email").val(),
diff --git a/erpnext/templates/includes/order/order_taxes.html b/erpnext/templates/includes/order/order_taxes.html
index d7b9620fa0a..42f46ac8d12 100644
--- a/erpnext/templates/includes/order/order_taxes.html
+++ b/erpnext/templates/includes/order/order_taxes.html
@@ -12,14 +12,14 @@
{% endif %}
{% for d in doc.taxes %}
- {% if d.base_tax_amount %}
+ {% if d.tax_amount %}
{{ d.description }}
- {{ d.get_formatted("base_tax_amount") }}
+ {{ d.get_formatted("tax_amount") }}
diff --git a/erpnext/templates/pages/order.html b/erpnext/templates/pages/order.html
index 388feb9eba9..ade66dd481f 100644
--- a/erpnext/templates/pages/order.html
+++ b/erpnext/templates/pages/order.html
@@ -40,7 +40,7 @@
- {{ _("Pay") }} {{doc.get_formatted("grand_total") }}
+ {{ _("Pay", null, "Amount") }} {{ pay_amount }}