diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py
index 82af85d7024..5f0faf79b5b 100644
--- a/erpnext/accounts/doctype/account/account.py
+++ b/erpnext/accounts/doctype/account/account.py
@@ -118,6 +118,7 @@ class Account(NestedSet):
self.validate_balance_must_be_debit_or_credit()
self.validate_account_currency()
self.validate_root_company_and_sync_account_to_children()
+ self.validate_receivable_payable_account_type()
def validate_parent_child_account_type(self):
if self.parent_account:
@@ -188,6 +189,24 @@ class Account(NestedSet):
"Balance Sheet" if self.root_type in ("Asset", "Liability", "Equity") else "Profit and Loss"
)
+ def validate_receivable_payable_account_type(self):
+ doc_before_save = self.get_doc_before_save()
+ receivable_payable_types = ["Receivable", "Payable"]
+ if (
+ doc_before_save
+ and doc_before_save.account_type in receivable_payable_types
+ and doc_before_save.account_type != self.account_type
+ ):
+ # check for ledger entries
+ if frappe.db.get_all("GL Entry", filters={"account": self.name, "is_cancelled": 0}, limit=1):
+ msg = _(
+ "There are ledger entries against this account. Changing {0} to non-{1} in live system will cause incorrect output in 'Accounts {2}' report"
+ ).format(
+ frappe.bold("Account Type"), doc_before_save.account_type, doc_before_save.account_type
+ )
+ frappe.msgprint(msg)
+ self.add_comment("Comment", msg)
+
def validate_root_details(self):
doc_before_save = self.get_doc_before_save()
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
index fd052d04760..0e238e08f62 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -1,7 +1,6 @@
{
"actions": [],
"creation": "2013-06-24 15:49:57",
- "description": "Settings for Accounts",
"doctype": "DocType",
"document_type": "Other",
"editable_grid": 1,
@@ -462,7 +461,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2023-11-20 09:37:47.650347",
+ "modified": "2024-01-30 14:04:26.553554",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py
index 4b99b198def..22a59bfde08 100644
--- a/erpnext/accounts/doctype/bank_account/bank_account.py
+++ b/erpnext/accounts/doctype/bank_account/bank_account.py
@@ -9,6 +9,7 @@ from frappe.contacts.address_and_contact import (
load_address_and_contact,
)
from frappe.model.document import Document
+from frappe.utils import comma_and, get_link_to_form
class BankAccount(Document):
@@ -52,6 +53,17 @@ class BankAccount(Document):
def validate(self):
self.validate_company()
self.validate_iban()
+ self.validate_account()
+
+ def validate_account(self):
+ if self.account:
+ if accounts := frappe.db.get_all("Bank Account", filters={"account": self.account}, as_list=1):
+ frappe.throw(
+ _("'{0}' account is already used by {1}. Use another account.").format(
+ frappe.bold(self.account),
+ frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
+ )
+ )
def validate_company(self):
if self.is_company_account and not self.company:
diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
index 4b97619f29f..8a505a8dee2 100644
--- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
+++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
@@ -5,7 +5,9 @@
import frappe
from frappe import _, msgprint
from frappe.model.document import Document
+from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, fmt_money, getdate
+from pypika import Order
import erpnext
@@ -179,39 +181,62 @@ def get_payment_entries_for_bank_clearance(
pos_sales_invoices, pos_purchase_invoices = [], []
if include_pos_transactions:
- pos_sales_invoices = frappe.db.sql(
- """
- select
- "Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
- si.posting_date, si.customer as against_account, sip.clearance_date,
- account.account_currency, 0 as credit
- from `tabSales Invoice Payment` sip, `tabSales Invoice` si, `tabAccount` account
- where
- sip.account=%(account)s and si.docstatus=1 and sip.parent = si.name
- and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s
- order by
- si.posting_date ASC, si.name DESC
- """,
- {"account": account, "from": from_date, "to": to_date},
- as_dict=1,
- )
+ si_payment = frappe.qb.DocType("Sales Invoice Payment")
+ si = frappe.qb.DocType("Sales Invoice")
+ acc = frappe.qb.DocType("Account")
- pos_purchase_invoices = frappe.db.sql(
- """
- select
- "Purchase Invoice" as payment_document, pi.name as payment_entry, pi.paid_amount as credit,
- pi.posting_date, pi.supplier as against_account, pi.clearance_date,
- account.account_currency, 0 as debit
- from `tabPurchase Invoice` pi, `tabAccount` account
- where
- pi.cash_bank_account=%(account)s and pi.docstatus=1 and account.name = pi.cash_bank_account
- and pi.posting_date >= %(from)s and pi.posting_date <= %(to)s
- order by
- pi.posting_date ASC, pi.name DESC
- """,
- {"account": account, "from": from_date, "to": to_date},
- as_dict=1,
- )
+ pos_sales_invoices = (
+ frappe.qb.from_(si_payment)
+ .inner_join(si)
+ .on(si_payment.parent == si.name)
+ .inner_join(acc)
+ .on(si_payment.account == acc.name)
+ .select(
+ ConstantColumn("Sales Invoice").as_("payment_document"),
+ si.name.as_("payment_entry"),
+ si_payment.reference_no.as_("cheque_number"),
+ si_payment.amount.as_("debit"),
+ si.posting_date,
+ si.customer.as_("against_account"),
+ si_payment.clearance_date,
+ acc.account_currency,
+ ConstantColumn(0).as_("credit"),
+ )
+ .where(
+ (si.docstatus == 1)
+ & (si_payment.account == account)
+ & (si.posting_date >= from_date)
+ & (si.posting_date <= to_date)
+ )
+ .orderby(si.posting_date)
+ .orderby(si.name, order=Order.desc)
+ ).run(as_dict=True)
+
+ pi = frappe.qb.DocType("Purchase Invoice")
+
+ pos_purchase_invoices = (
+ frappe.qb.from_(pi)
+ .inner_join(acc)
+ .on(pi.cash_bank_account == acc.name)
+ .select(
+ ConstantColumn("Purchase Invoice").as_("payment_document"),
+ pi.name.as_("payment_entry"),
+ pi.paid_amount.as_("credit"),
+ pi.posting_date,
+ pi.supplier.as_("against_account"),
+ pi.clearance_date,
+ acc.account_currency,
+ ConstantColumn(0).as_("debit"),
+ )
+ .where(
+ (pi.docstatus == 1)
+ & (pi.cash_bank_account == account)
+ & (pi.posting_date >= from_date)
+ & (pi.posting_date <= to_date)
+ )
+ .orderby(pi.posting_date)
+ .orderby(pi.name, order=Order.desc)
+ ).run(as_dict=True)
entries = (
list(payment_entries)
diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
index 7bb3f4183b4..1fe3608f566 100644
--- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
@@ -32,8 +32,16 @@ class TestBankTransaction(FrappeTestCase):
frappe.db.delete(dt)
clear_loan_transactions()
make_pos_profile()
- add_transactions()
- add_vouchers()
+
+ # generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error
+ uniq_identifier = frappe.generate_hash(length=10)
+ gl_account = create_gl_account("_Test Bank " + uniq_identifier)
+ bank_account = create_bank_account(
+ gl_account=gl_account, bank_account_name="Checking Account " + uniq_identifier
+ )
+
+ add_transactions(bank_account=bank_account)
+ add_vouchers(gl_account=gl_account)
# This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction.
def test_linked_payments(self):
@@ -219,7 +227,9 @@ def clear_loan_transactions():
frappe.db.delete("Loan Repayment")
-def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
+def create_bank_account(
+ bank_name="Citi Bank", gl_account="_Test Bank - _TC", bank_account_name="Checking Account"
+):
try:
frappe.get_doc(
{
@@ -231,21 +241,35 @@ def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
pass
try:
- frappe.get_doc(
+ bank_account = frappe.get_doc(
{
"doctype": "Bank Account",
- "account_name": "Checking Account",
+ "account_name": bank_account_name,
"bank": bank_name,
- "account": account_name,
+ "account": gl_account,
}
).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
+ return bank_account.name
-def add_transactions():
- create_bank_account()
+def create_gl_account(gl_account_name="_Test Bank - _TC"):
+ gl_account = frappe.get_doc(
+ {
+ "doctype": "Account",
+ "company": "_Test Company",
+ "parent_account": "Current Assets - _TC",
+ "account_type": "Bank",
+ "is_group": 0,
+ "account_name": gl_account_name,
+ }
+ ).insert()
+ return gl_account.name
+
+
+def add_transactions(bank_account="_Test Bank - _TC"):
doc = frappe.get_doc(
{
"doctype": "Bank Transaction",
@@ -253,7 +277,7 @@ def add_transactions():
"date": "2018-10-23",
"deposit": 1200,
"currency": "INR",
- "bank_account": "Checking Account - Citi Bank",
+ "bank_account": bank_account,
}
).insert()
doc.submit()
@@ -265,7 +289,7 @@ def add_transactions():
"date": "2018-10-23",
"deposit": 1700,
"currency": "INR",
- "bank_account": "Checking Account - Citi Bank",
+ "bank_account": bank_account,
}
).insert()
doc.submit()
@@ -277,7 +301,7 @@ def add_transactions():
"date": "2018-10-26",
"withdrawal": 690,
"currency": "INR",
- "bank_account": "Checking Account - Citi Bank",
+ "bank_account": bank_account,
}
).insert()
doc.submit()
@@ -289,7 +313,7 @@ def add_transactions():
"date": "2018-10-27",
"deposit": 3900,
"currency": "INR",
- "bank_account": "Checking Account - Citi Bank",
+ "bank_account": bank_account,
}
).insert()
doc.submit()
@@ -301,13 +325,13 @@ def add_transactions():
"date": "2018-10-27",
"withdrawal": 109080,
"currency": "INR",
- "bank_account": "Checking Account - Citi Bank",
+ "bank_account": bank_account,
}
).insert()
doc.submit()
-def add_vouchers():
+def add_vouchers(gl_account="_Test Bank - _TC"):
try:
frappe.get_doc(
{
@@ -323,7 +347,7 @@ def add_vouchers():
pi = make_purchase_invoice(supplier="Conrad Electronic", qty=1, rate=690)
- pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
+ pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
pe.reference_no = "Conrad Oct 18"
pe.reference_date = "2018-10-24"
pe.insert()
@@ -342,14 +366,14 @@ def add_vouchers():
pass
pi = make_purchase_invoice(supplier="Mr G", qty=1, rate=1200)
- pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
+ pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
pe.reference_no = "Herr G Oct 18"
pe.reference_date = "2018-10-24"
pe.insert()
pe.submit()
pi = make_purchase_invoice(supplier="Mr G", qty=1, rate=1700)
- pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
+ pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
pe.reference_no = "Herr G Nov 18"
pe.reference_date = "2018-11-01"
pe.insert()
@@ -380,10 +404,10 @@ def add_vouchers():
pass
pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900, is_paid=1, do_not_save=1)
- pi.cash_bank_account = "_Test Bank - _TC"
+ pi.cash_bank_account = gl_account
pi.insert()
pi.submit()
- pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
+ pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
pe.reference_no = "Poore Simon's Oct 18"
pe.reference_date = "2018-10-28"
pe.paid_amount = 690
@@ -392,7 +416,7 @@ def add_vouchers():
pe.submit()
si = create_sales_invoice(customer="Poore Simon's", qty=1, rate=3900)
- pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
+ pe = get_payment_entry("Sales Invoice", si.name, bank_account=gl_account)
pe.reference_no = "Poore Simon's Oct 18"
pe.reference_date = "2018-10-28"
pe.insert()
@@ -415,16 +439,12 @@ def add_vouchers():
if not frappe.db.get_value(
"Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}
):
- mode_of_payment.append(
- "accounts", {"company": "_Test Company", "default_account": "_Test Bank - _TC"}
- )
+ mode_of_payment.append("accounts", {"company": "_Test Company", "default_account": gl_account})
mode_of_payment.save()
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1)
si.is_pos = 1
- si.append(
- "payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 109080}
- )
+ si.append("payments", {"mode_of_payment": "Cash", "account": gl_account, "amount": 109080})
si.insert()
si.submit()
diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json
index 5ab91f2506c..da7f08fe3d4 100644
--- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json
+++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json
@@ -3,7 +3,7 @@
"allow_import": 1,
"autoname": "field:year",
"creation": "2013-01-22 16:50:25",
- "description": "**Fiscal Year** represents a Financial Year. All accounting entries and other major transactions are tracked against **Fiscal Year**.",
+ "description": "Represents a Financial Year. All accounting entries and other major transactions are tracked against the Fiscal Year.",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
@@ -82,10 +82,11 @@
"icon": "fa fa-calendar",
"idx": 1,
"links": [],
- "modified": "2020-11-05 12:16:53.081573",
+ "modified": "2024-01-30 12:35:38.645968",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Fiscal Year",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -122,5 +123,6 @@
],
"show_name_in_global_search": 1,
"sort_field": "name",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index 139f52696bc..17a97a05529 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -13,16 +13,9 @@ import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
)
-from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
- get_dimension_filter_map,
-)
from erpnext.accounts.party import validate_party_frozen_disabled, validate_party_gle_currency
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
-from erpnext.exceptions import (
- InvalidAccountCurrency,
- InvalidAccountDimensionError,
- MandatoryAccountDimensionError,
-)
+from erpnext.exceptions import InvalidAccountCurrency
exclude_from_linked_with = True
@@ -98,7 +91,6 @@ class GLEntry(Document):
if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
self.validate_account_details(adv_adj)
self.validate_dimensions_for_pl_and_bs()
- self.validate_allowed_dimensions()
validate_balance_type(self.account, adv_adj)
validate_frozen_account(self.account, adv_adj)
@@ -208,42 +200,6 @@ class GLEntry(Document):
)
)
- def validate_allowed_dimensions(self):
- dimension_filter_map = get_dimension_filter_map()
- for key, value in dimension_filter_map.items():
- dimension = key[0]
- account = key[1]
-
- if self.account == account:
- if value["is_mandatory"] and not self.get(dimension):
- frappe.throw(
- _("{0} is mandatory for account {1}").format(
- frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)
- ),
- MandatoryAccountDimensionError,
- )
-
- if value["allow_or_restrict"] == "Allow":
- if self.get(dimension) and self.get(dimension) not in value["allowed_dimensions"]:
- frappe.throw(
- _("Invalid value {0} for {1} against account {2}").format(
- frappe.bold(self.get(dimension)),
- frappe.bold(frappe.unscrub(dimension)),
- frappe.bold(self.account),
- ),
- InvalidAccountDimensionError,
- )
- else:
- if self.get(dimension) and self.get(dimension) in value["allowed_dimensions"]:
- frappe.throw(
- _("Invalid value {0} for {1} against account {2}").format(
- frappe.bold(self.get(dimension)),
- frappe.bold(frappe.unscrub(dimension)),
- frappe.bold(self.account),
- ),
- InvalidAccountDimensionError,
- )
-
def check_pl_account(self):
if (
self.is_opening == "Yes"
diff --git a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.json b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.json
index 14f2d802505..488e8b26207 100644
--- a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.json
+++ b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.json
@@ -1,173 +1,77 @@
{
- "allow_copy": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "field:distribution_id",
- "beta": 0,
- "creation": "2013-01-10 16:34:05",
- "custom": 0,
- "description": "**Monthly Distribution** helps you distribute the Budget/Target across months if you have seasonality in your business.",
- "docstatus": 0,
- "doctype": "DocType",
- "editable_grid": 0,
- "engine": "InnoDB",
+ "actions": [],
+ "autoname": "field:distribution_id",
+ "creation": "2013-01-10 16:34:05",
+ "description": "Helps you distribute the Budget/Target across months if you have seasonality in your business.",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "distribution_id",
+ "fiscal_year",
+ "percentages"
+ ],
"fields": [
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "Name of the Monthly Distribution",
- "fieldname": "distribution_id",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Distribution Name",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "distribution_id",
- "oldfieldtype": "Data",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "description": "Name of the Monthly Distribution",
+ "fieldname": "distribution_id",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Distribution Name",
+ "oldfieldname": "distribution_id",
+ "oldfieldtype": "Data",
+ "reqd": 1,
+ "unique": 1
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "fiscal_year",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 1,
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Fiscal Year",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "fiscal_year",
- "oldfieldtype": "Select",
- "options": "Fiscal Year",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 1,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "fiscal_year",
+ "fieldtype": "Link",
+ "in_filter": 1,
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Fiscal Year",
+ "oldfieldname": "fiscal_year",
+ "oldfieldtype": "Select",
+ "options": "Fiscal Year",
+ "search_index": 1
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "percentages",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Monthly Distribution Percentages",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "budget_distribution_details",
- "oldfieldtype": "Table",
- "options": "Monthly Distribution Percentage",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "fieldname": "percentages",
+ "fieldtype": "Table",
+ "label": "Monthly Distribution Percentages",
+ "oldfieldname": "budget_distribution_details",
+ "oldfieldtype": "Table",
+ "options": "Monthly Distribution Percentage"
}
- ],
- "hide_heading": 0,
- "hide_toolbar": 0,
- "icon": "fa fa-bar-chart",
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
-
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2016-11-21 14:54:35.998761",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Monthly Distribution",
- "name_case": "Title Case",
- "owner": "Administrator",
+ ],
+ "icon": "fa fa-bar-chart",
+ "idx": 1,
+ "links": [],
+ "modified": "2024-01-30 13:57:55.802744",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Monthly Distribution",
+ "naming_rule": "By fieldname",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "is_custom": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Accounts Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "is_custom": 0,
- "permlevel": 2,
- "print": 0,
- "read": 1,
- "report": 1,
- "role": "Accounts Manager",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
+ "permlevel": 2,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager"
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_seen": 0
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index cd2ad394763..8af0713b0b4 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -946,19 +946,19 @@ class PaymentEntry(AccountsController):
)
base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount)
-
- if self.payment_type == "Receive":
- self.difference_amount = base_party_amount - self.base_received_amount
- elif self.payment_type == "Pay":
- self.difference_amount = self.base_paid_amount - base_party_amount
- else:
- self.difference_amount = self.base_paid_amount - flt(self.base_received_amount)
-
- total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
included_taxes = self.get_included_taxes()
+ if self.payment_type == "Receive":
+ self.difference_amount = base_party_amount - self.base_received_amount + included_taxes
+ elif self.payment_type == "Pay":
+ self.difference_amount = self.base_paid_amount - base_party_amount - included_taxes
+ else:
+ self.difference_amount = self.base_paid_amount - flt(self.base_received_amount) - included_taxes
+
+ total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
+
self.difference_amount = flt(
- self.difference_amount - total_deductions - included_taxes, self.precision("difference_amount")
+ self.difference_amount - total_deductions, self.precision("difference_amount")
)
def get_included_taxes(self):
diff --git a/erpnext/accounts/doctype/payment_order/test_payment_order.py b/erpnext/accounts/doctype/payment_order/test_payment_order.py
index 0dcb1794b9a..60f288e1f07 100644
--- a/erpnext/accounts/doctype/payment_order/test_payment_order.py
+++ b/erpnext/accounts/doctype/payment_order/test_payment_order.py
@@ -4,9 +4,13 @@
import unittest
import frappe
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import getdate
-from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
+from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import (
+ create_bank_account,
+ create_gl_account,
+)
from erpnext.accounts.doctype.payment_entry.payment_entry import (
get_payment_entry,
make_payment_order,
@@ -14,28 +18,32 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import (
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
-class TestPaymentOrder(unittest.TestCase):
+class TestPaymentOrder(FrappeTestCase):
def setUp(self):
- create_bank_account()
+ # generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error
+ uniq_identifier = frappe.generate_hash(length=10)
+ self.gl_account = create_gl_account("_Test Bank " + uniq_identifier)
+ self.bank_account = create_bank_account(
+ gl_account=self.gl_account, bank_account_name="Checking Account " + uniq_identifier
+ )
def tearDown(self):
- for bt in frappe.get_all("Payment Order"):
- doc = frappe.get_doc("Payment Order", bt.name)
- doc.cancel()
- doc.delete()
+ frappe.db.rollback()
def test_payment_order_creation_against_payment_entry(self):
purchase_invoice = make_purchase_invoice()
payment_entry = get_payment_entry(
- "Purchase Invoice", purchase_invoice.name, bank_account="_Test Bank - _TC"
+ "Purchase Invoice", purchase_invoice.name, bank_account=self.gl_account
)
payment_entry.reference_no = "_Test_Payment_Order"
payment_entry.reference_date = getdate()
- payment_entry.party_bank_account = "Checking Account - Citi Bank"
+ payment_entry.party_bank_account = self.bank_account
payment_entry.insert()
payment_entry.submit()
- doc = create_payment_order_against_payment_entry(payment_entry, "Payment Entry")
+ doc = create_payment_order_against_payment_entry(
+ payment_entry, "Payment Entry", self.bank_account
+ )
reference_doc = doc.get("references")[0]
self.assertEqual(reference_doc.reference_name, payment_entry.name)
self.assertEqual(reference_doc.reference_doctype, "Payment Entry")
@@ -43,13 +51,13 @@ class TestPaymentOrder(unittest.TestCase):
self.assertEqual(reference_doc.amount, 250)
-def create_payment_order_against_payment_entry(ref_doc, order_type):
+def create_payment_order_against_payment_entry(ref_doc, order_type, bank_account):
payment_order = frappe.get_doc(
dict(
doctype="Payment Order",
company="_Test Company",
payment_order_type=order_type,
- company_bank_account="Checking Account - Citi Bank",
+ company_bank_account=bank_account,
)
)
doc = make_payment_order(ref_doc.name, payment_order)
diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
index 5a281aaa4fd..ad2889d0a0a 100644
--- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
+++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
@@ -80,13 +80,16 @@
"target_warehouse",
"quality_inspection",
"serial_and_batch_bundle",
- "batch_no",
+ "use_serial_batch_fields",
"col_break5",
"allow_zero_valuation_rate",
- "serial_no",
"item_tax_rate",
"actual_batch_qty",
"actual_qty",
+ "section_break_tlhi",
+ "serial_no",
+ "column_break_ciit",
+ "batch_no",
"edit_references",
"sales_order",
"so_detail",
@@ -628,13 +631,13 @@
"options": "Quality Inspection"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
"hidden": 1,
"label": "Batch No",
"options": "Batch",
- "print_hide": 1,
- "read_only": 1
+ "print_hide": 1
},
{
"fieldname": "col_break5",
@@ -649,14 +652,14 @@
"print_hide": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_no",
"fieldtype": "Small Text",
"hidden": 1,
"in_list_view": 1,
"label": "Serial No",
"oldfieldname": "serial_no",
- "oldfieldtype": "Small Text",
- "read_only": 1
+ "oldfieldtype": "Small Text"
},
{
"fieldname": "item_tax_rate",
@@ -824,17 +827,33 @@
"read_only": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "use_serial_batch_fields",
+ "fieldtype": "Check",
+ "label": "Use Serial No / Batch Fields"
+ },
+ {
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
+ "fieldname": "section_break_tlhi",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_ciit",
+ "fieldtype": "Column Break"
}
],
"istable": 1,
"links": [],
- "modified": "2023-11-14 18:33:22.585715",
+ "modified": "2024-02-04 16:36:25.665743",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Item",
diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py
index e2a62f1336e..55a577b0c51 100644
--- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py
+++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py
@@ -82,6 +82,7 @@ class POSInvoiceItem(Document):
target_warehouse: DF.Link | None
total_weight: DF.Float
uom: DF.Link
+ use_serial_batch_fields: DF.Check
warehouse: DF.Link | None
weight_per_unit: DF.Float
weight_uom: DF.Link | None
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
index b41cf53665e..4ccfc192ac3 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
@@ -579,12 +579,17 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
item_details[field] += pricing_rule.get(field, 0) if pricing_rule else args.get(field, 0)
+@frappe.whitelist()
def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, rate=None):
from erpnext.accounts.doctype.pricing_rule.utils import (
get_applied_pricing_rules,
get_pricing_rule_items,
)
+ if isinstance(item_details, str):
+ item_details = json.loads(item_details)
+ item_details = frappe._dict(item_details)
+
for d in get_applied_pricing_rules(pricing_rules):
if not d or not frappe.db.exists("Pricing Rule", d):
continue
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 c03b18a8716..083c8fce18b 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
@@ -120,18 +120,6 @@ def get_statement_dict(doc, get_statement_dict=False):
statement_dict = {}
ageing = ""
- err_journals = None
- if doc.report == "General Ledger" and doc.ignore_exchange_rate_revaluation_journals:
- err_journals = frappe.db.get_all(
- "Journal Entry",
- filters={
- "company": doc.company,
- "docstatus": 1,
- "voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]),
- },
- as_list=True,
- )
-
for entry in doc.customers:
if doc.include_ageing:
ageing = set_ageing(doc, entry)
@@ -144,8 +132,8 @@ def get_statement_dict(doc, get_statement_dict=False):
)
filters = get_common_filters(doc)
- if err_journals:
- filters.update({"voucher_no_not_in": [x[0] for x in err_journals]})
+ if doc.ignore_exchange_rate_revaluation_journals:
+ filters.update({"ignore_err": True})
if doc.report == "General Ledger":
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index cb0b8e1fb11..45b24826400 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -696,6 +696,7 @@ class PurchaseInvoice(BuyingController):
# Updating stock ledger should always be called after updating prevdoc status,
# because updating ordered qty in bin depends upon updated ordered qty in PO
if self.update_stock == 1:
+ self.make_bundle_using_old_serial_batch_fields()
self.update_stock_ledger()
if self.is_old_subcontracting_flow:
diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
index 26984d96efd..3ee4214ae71 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
@@ -62,16 +62,19 @@
"rm_supp_cost",
"warehouse_section",
"warehouse",
- "from_warehouse",
- "quality_inspection",
"add_serial_batch_bundle",
"serial_and_batch_bundle",
- "serial_no",
+ "use_serial_batch_fields",
"col_br_wh",
+ "from_warehouse",
+ "quality_inspection",
"rejected_warehouse",
"rejected_serial_and_batch_bundle",
- "batch_no",
+ "section_break_rqbe",
+ "serial_no",
"rejected_serial_no",
+ "column_break_vbbb",
+ "batch_no",
"manufacture_details",
"manufacturer",
"column_break_13",
@@ -440,13 +443,11 @@
"print_hide": 1
},
{
- "depends_on": "eval:!doc.is_fixed_asset",
+ "depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
- "hidden": 1,
"label": "Batch No",
"options": "Batch",
- "read_only": 1,
"search_index": 1
},
{
@@ -454,21 +455,18 @@
"fieldtype": "Column Break"
},
{
- "depends_on": "eval:!doc.is_fixed_asset",
+ "depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "serial_no",
"fieldtype": "Text",
- "hidden": 1,
- "label": "Serial No",
- "read_only": 1
+ "label": "Serial No"
},
{
- "depends_on": "eval:!doc.is_fixed_asset",
+ "depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "rejected_serial_no",
"fieldtype": "Text",
"label": "Rejected Serial No",
"no_copy": 1,
- "print_hide": 1,
- "read_only": 1
+ "print_hide": 1
},
{
"fieldname": "accounting",
@@ -891,7 +889,7 @@
"label": "Apply TDS"
},
{
- "depends_on": "eval:parent.update_stock == 1",
+ "depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
@@ -901,7 +899,7 @@
"search_index": 1
},
{
- "depends_on": "eval:parent.update_stock == 1",
+ "depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
"fieldname": "rejected_serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Rejected Serial and Batch Bundle",
@@ -916,16 +914,31 @@
"options": "Asset"
},
{
- "depends_on": "eval:parent.update_stock === 1",
+ "depends_on": "eval:parent.update_stock === 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
"fieldname": "add_serial_batch_bundle",
"fieldtype": "Button",
"label": "Add Serial / Batch No"
+ },
+ {
+ "default": "0",
+ "fieldname": "use_serial_batch_fields",
+ "fieldtype": "Check",
+ "label": "Use Serial No / Batch Fields"
+ },
+ {
+ "depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
+ "fieldname": "section_break_rqbe",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_vbbb",
+ "fieldtype": "Column Break"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2024-01-21 19:46:25.537861",
+ "modified": "2024-02-04 14:11:52.742228",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py
index e48d22379a6..ccbc34749d7 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py
@@ -88,6 +88,7 @@ class PurchaseInvoiceItem(Document):
stock_uom_rate: DF.Currency
total_weight: DF.Float
uom: DF.Link
+ use_serial_batch_fields: DF.Check
valuation_rate: DF.Currency
warehouse: DF.Link | None
weight_per_unit: DF.Float
diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json
index c36efb89a30..2ff6a45d1eb 100644
--- a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json
+++ b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json
@@ -3,7 +3,7 @@
"allow_import": 1,
"allow_rename": 1,
"creation": "2013-01-10 16:34:08",
- "description": "Standard tax template that can be applied to all Purchase Transactions. This template can contain list of tax heads and also other expense heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Consider Tax or Charge for: In this section you can specify if the tax / charge is only for valuation (not a part of total) or only for total (does not add value to the item) or for both.\n10. Add or Deduct: Whether you want to add or deduct the tax.",
+ "description": "Standard tax template that can be applied to all Purchase Transactions. This template can contain a list of tax heads and also other expense heads like \"Shipping\", \"Insurance\", \"Handling\", etc.",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
@@ -77,7 +77,7 @@
"icon": "fa fa-money",
"idx": 1,
"links": [],
- "modified": "2022-05-16 16:15:29.059370",
+ "modified": "2024-01-30 13:08:09.537242",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Taxes and Charges Template",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 343f3033bfb..bbfe6a38d8d 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -446,6 +446,7 @@ class SalesInvoice(SellingController):
# Updating stock ledger should always be called after updating prevdoc status,
# because updating reserved qty in bin depends upon updated delivered qty in SO
if self.update_stock == 1:
+ self.make_bundle_using_old_serial_batch_fields()
self.update_stock_ledger()
# this sequence because outstanding may get -ve
diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
index ec9e792d7d4..d06c7861da7 100644
--- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
+++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
@@ -83,14 +83,17 @@
"quality_inspection",
"pick_serial_and_batch",
"serial_and_batch_bundle",
- "batch_no",
- "incoming_rate",
+ "use_serial_batch_fields",
"col_break5",
"allow_zero_valuation_rate",
- "serial_no",
+ "incoming_rate",
"item_tax_rate",
"actual_batch_qty",
"actual_qty",
+ "section_break_eoec",
+ "serial_no",
+ "column_break_ytgd",
+ "batch_no",
"edit_references",
"sales_order",
"so_detail",
@@ -600,12 +603,11 @@
"options": "Quality Inspection"
},
{
+ "depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
- "hidden": 1,
"label": "Batch No",
"options": "Batch",
- "read_only": 1,
"search_index": 1
},
{
@@ -621,13 +623,12 @@
"print_hide": 1
},
{
+ "depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "serial_no",
"fieldtype": "Small Text",
- "hidden": 1,
"label": "Serial No",
"oldfieldname": "serial_no",
- "oldfieldtype": "Small Text",
- "read_only": 1
+ "oldfieldtype": "Small Text"
},
{
"fieldname": "item_group",
@@ -891,6 +892,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
@@ -904,12 +906,27 @@
"fieldname": "pick_serial_and_batch",
"fieldtype": "Button",
"label": "Pick Serial / Batch No"
+ },
+ {
+ "default": "0",
+ "fieldname": "use_serial_batch_fields",
+ "fieldtype": "Check",
+ "label": "Use Serial No / Batch Fields"
+ },
+ {
+ "depends_on": "eval:doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
+ "fieldname": "section_break_eoec",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_ytgd",
+ "fieldtype": "Column Break"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2023-12-29 13:03:14.121298",
+ "modified": "2024-02-04 11:52:16.106541",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py
index 80f67748f40..c71d08e7f70 100644
--- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py
+++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py
@@ -86,6 +86,7 @@ class SalesInvoiceItem(Document):
target_warehouse: DF.Link | None
total_weight: DF.Float
uom: DF.Link
+ use_serial_batch_fields: DF.Check
warehouse: DF.Link | None
weight_per_unit: DF.Float
weight_uom: DF.Link | None
diff --git a/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.json b/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.json
index 5ab46b7fd5c..bd59f65dd4c 100644
--- a/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.json
+++ b/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.json
@@ -8,6 +8,7 @@
"default",
"mode_of_payment",
"amount",
+ "reference_no",
"column_break_3",
"account",
"type",
@@ -75,11 +76,16 @@
"hidden": 1,
"label": "Default",
"read_only": 1
+ },
+ {
+ "fieldname": "reference_no",
+ "fieldtype": "Data",
+ "label": "Reference No"
}
],
"istable": 1,
"links": [],
- "modified": "2020-08-03 12:45:39.986598",
+ "modified": "2024-01-23 16:20:06.436979",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Payment",
@@ -87,5 +93,6 @@
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.py b/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.py
index 57d01424063..e460a01155e 100644
--- a/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.py
+++ b/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.py
@@ -23,6 +23,7 @@ class SalesInvoicePayment(Document):
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
+ reference_no: DF.Data | None
type: DF.ReadOnly | None
# end: auto-generated types
diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json
index 408ecbf36dc..736d283cdbd 100644
--- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json
+++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json
@@ -3,7 +3,7 @@
"allow_import": 1,
"allow_rename": 1,
"creation": "2013-01-10 16:34:09",
- "description": "Standard tax template that can be applied to all Sales Transactions. This template can contain list of tax heads and also other expense / income heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Is this Tax included in Basic Rate?: If you check this, it means that this tax will not be shown below the item table, but will be included in the Basic Rate in your main item table. This is useful where you want give a flat price (inclusive of all taxes) price to customers.",
+ "description": "Standard tax template that can be applied to all Sales Transactions. This template can contain a list of tax heads and also other expense/income heads like \"Shipping\", \"Insurance\", \"Handling\" etc.",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
@@ -79,7 +79,7 @@
"icon": "fa fa-money",
"idx": 1,
"links": [],
- "modified": "2022-05-16 16:14:52.061672",
+ "modified": "2024-01-30 13:07:28.801104",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Taxes and Charges Template",
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 37ee5de1c9d..9f5c954fa48 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -13,9 +13,13 @@ import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
+from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
+ get_dimension_filter_map,
+)
from erpnext.accounts.doctype.accounting_period.accounting_period import ClosedAccountingPeriod
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
from erpnext.accounts.utils import create_payment_ledger_entry
+from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
def make_gl_entries(
@@ -355,6 +359,7 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
process_debit_credit_difference(gl_map)
+ dimension_filter_map = get_dimension_filter_map()
if gl_map:
check_freezing_date(gl_map[0]["posting_date"], adv_adj)
is_opening = any(d.get("is_opening") == "Yes" for d in gl_map)
@@ -362,6 +367,7 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
validate_against_pcv(is_opening, gl_map[0]["posting_date"], gl_map[0]["company"])
for entry in gl_map:
+ validate_allowed_dimensions(entry, dimension_filter_map)
make_entry(entry, adv_adj, update_outstanding, from_repost)
@@ -700,3 +706,39 @@ def set_as_cancel(voucher_type, voucher_no):
where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
(now(), frappe.session.user, voucher_type, voucher_no),
)
+
+
+def validate_allowed_dimensions(gl_entry, dimension_filter_map):
+ for key, value in dimension_filter_map.items():
+ dimension = key[0]
+ account = key[1]
+
+ if gl_entry.account == account:
+ if value["is_mandatory"] and not gl_entry.get(dimension):
+ frappe.throw(
+ _("{0} is mandatory for account {1}").format(
+ frappe.bold(frappe.unscrub(dimension)), frappe.bold(gl_entry.account)
+ ),
+ MandatoryAccountDimensionError,
+ )
+
+ if value["allow_or_restrict"] == "Allow":
+ if gl_entry.get(dimension) and gl_entry.get(dimension) not in value["allowed_dimensions"]:
+ frappe.throw(
+ _("Invalid value {0} for {1} against account {2}").format(
+ frappe.bold(gl_entry.get(dimension)),
+ frappe.bold(frappe.unscrub(dimension)),
+ frappe.bold(gl_entry.account),
+ ),
+ InvalidAccountDimensionError,
+ )
+ else:
+ if gl_entry.get(dimension) and gl_entry.get(dimension) in value["allowed_dimensions"]:
+ frappe.throw(
+ _("Invalid value {0} for {1} against account {2}").format(
+ frappe.bold(gl_entry.get(dimension)),
+ frappe.bold(frappe.unscrub(dimension)),
+ frappe.bold(gl_entry.account),
+ ),
+ InvalidAccountDimensionError,
+ )
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js
index 79b5e4d9ec8..b7b9d34e00f 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.js
+++ b/erpnext/accounts/report/general_ledger/general_ledger.js
@@ -203,8 +203,14 @@ frappe.query_reports["General Ledger"] = {
"fieldname": "show_remarks",
"label": __("Show Remarks"),
"fieldtype": "Check"
+ },
+ {
+ "fieldname": "ignore_err",
+ "label": __("Ignore Exchange Rate Revaluation Journals"),
+ "fieldtype": "Check"
}
+
]
}
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index ff6cd9f4b25..8fccc14873e 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -241,6 +241,19 @@ def get_conditions(filters):
if filters.get("against_voucher_no"):
conditions.append("against_voucher=%(against_voucher_no)s")
+ if filters.get("ignore_err"):
+ err_journals = frappe.db.get_all(
+ "Journal Entry",
+ filters={
+ "company": filters.get("company"),
+ "docstatus": 1,
+ "voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]),
+ },
+ as_list=True,
+ )
+ if err_journals:
+ filters.update({"voucher_no_not_in": [x[0] for x in err_journals]})
+
if filters.get("voucher_no_not_in"):
conditions.append("voucher_no not in %(voucher_no_not_in)s")
diff --git a/erpnext/accounts/report/general_ledger/test_general_ledger.py b/erpnext/accounts/report/general_ledger/test_general_ledger.py
index a8c362e78c1..75f94309bcc 100644
--- a/erpnext/accounts/report/general_ledger/test_general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/test_general_ledger.py
@@ -3,7 +3,7 @@
import frappe
from frappe.tests.utils import FrappeTestCase
-from frappe.utils import today
+from frappe.utils import flt, today
from erpnext.accounts.report.general_ledger.general_ledger import execute
@@ -148,3 +148,105 @@ class TestGeneralLedger(FrappeTestCase):
self.assertEqual(data[2]["credit"], 900)
self.assertEqual(data[3]["debit"], 100)
self.assertEqual(data[3]["credit"], 100)
+
+ def test_ignore_exchange_rate_journals_filter(self):
+ # create a new account with USD currency
+ account_name = "Test Debtors USD"
+ company = "_Test Company"
+ account = frappe.get_doc(
+ {
+ "account_name": account_name,
+ "is_group": 0,
+ "company": company,
+ "root_type": "Asset",
+ "report_type": "Balance Sheet",
+ "account_currency": "USD",
+ "parent_account": "Accounts Receivable - _TC",
+ "account_type": "Receivable",
+ "doctype": "Account",
+ }
+ )
+ account.insert(ignore_if_duplicate=True)
+ # create a JV to debit 1000 USD at 75 exchange rate
+ jv = frappe.new_doc("Journal Entry")
+ jv.posting_date = today()
+ jv.company = company
+ jv.multi_currency = 1
+ jv.cost_center = "_Test Cost Center - _TC"
+ jv.set(
+ "accounts",
+ [
+ {
+ "account": account.name,
+ "party_type": "Customer",
+ "party": "_Test Customer USD",
+ "debit_in_account_currency": 1000,
+ "credit_in_account_currency": 0,
+ "exchange_rate": 75,
+ "cost_center": "_Test Cost Center - _TC",
+ },
+ {
+ "account": "Cash - _TC",
+ "debit_in_account_currency": 0,
+ "credit_in_account_currency": 75000,
+ "cost_center": "_Test Cost Center - _TC",
+ },
+ ],
+ )
+ jv.save()
+ jv.submit()
+
+ revaluation = frappe.new_doc("Exchange Rate Revaluation")
+ revaluation.posting_date = today()
+ revaluation.company = company
+ accounts = revaluation.get_accounts_data()
+ revaluation.extend("accounts", accounts)
+ row = revaluation.accounts[0]
+ row.new_exchange_rate = 83
+ row.new_balance_in_base_currency = flt(
+ row.new_exchange_rate * flt(row.balance_in_account_currency)
+ )
+ row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency)
+ revaluation.set_total_gain_loss()
+ revaluation = revaluation.save().submit()
+
+ # post journal entry for Revaluation doc
+ frappe.db.set_value(
+ "Company", company, "unrealized_exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
+ )
+ revaluation_jv = revaluation.make_jv_for_revaluation()
+ revaluation_jv.cost_center = "_Test Cost Center - _TC"
+ for acc in revaluation_jv.get("accounts"):
+ acc.cost_center = "_Test Cost Center - _TC"
+ revaluation_jv.save()
+ revaluation_jv.submit()
+
+ # With ignore_err enabled
+ columns, data = execute(
+ frappe._dict(
+ {
+ "company": company,
+ "from_date": today(),
+ "to_date": today(),
+ "account": [account.name],
+ "group_by": "Group by Voucher (Consolidated)",
+ "ignore_err": True,
+ }
+ )
+ )
+ self.assertNotIn(revaluation_jv.name, set([x.voucher_no for x in data]))
+
+ # Without ignore_err enabled
+ columns, data = execute(
+ frappe._dict(
+ {
+ "company": company,
+ "from_date": today(),
+ "to_date": today(),
+ "account": [account.name],
+ "group_by": "Group by Voucher (Consolidated)",
+ "ignore_err": False,
+ }
+ )
+ )
+ self.assertIn(revaluation_jv.name, set([x.voucher_no for x in data]))
diff --git a/erpnext/accounts/report/trial_balance/trial_balance.js b/erpnext/accounts/report/trial_balance/trial_balance.js
index 2c4c7620736..5374ac16d11 100644
--- a/erpnext/accounts/report/trial_balance/trial_balance.js
+++ b/erpnext/accounts/report/trial_balance/trial_balance.js
@@ -78,8 +78,14 @@ frappe.query_reports["Trial Balance"] = {
"options": erpnext.get_presentation_currency_list()
},
{
- "fieldname": "with_period_closing_entry",
- "label": __("Period Closing Entry"),
+ "fieldname": "with_period_closing_entry_for_opening",
+ "label": __("With Period Closing Entry For Opening Balances"),
+ "fieldtype": "Check",
+ "default": 1
+ },
+ {
+ "fieldname": "with_period_closing_entry_for_current_period",
+ "label": __("Period Closing Entry For Current Period"),
"fieldtype": "Check",
"default": 1
},
diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py
index 8b7f0bbc006..2ff0eff662d 100644
--- a/erpnext/accounts/report/trial_balance/trial_balance.py
+++ b/erpnext/accounts/report/trial_balance/trial_balance.py
@@ -116,7 +116,7 @@ def get_data(filters):
max_rgt,
filters,
gl_entries_by_account,
- ignore_closing_entries=not flt(filters.with_period_closing_entry),
+ ignore_closing_entries=not flt(filters.with_period_closing_entry_for_current_period),
ignore_opening_entries=True,
)
@@ -249,7 +249,7 @@ def get_opening_balance(
):
opening_balance = opening_balance.where(closing_balance.posting_date >= filters.year_start_date)
- if not flt(filters.with_period_closing_entry):
+ if not flt(filters.with_period_closing_entry_for_opening):
if doctype == "Account Closing Balance":
opening_balance = opening_balance.where(closing_balance.is_period_closing_voucher_entry == 0)
else:
diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
index cad74df51e1..66014904cc4 100644
--- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
+++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
@@ -126,6 +126,7 @@ class AssetCapitalization(StockController):
self.create_target_asset()
def on_submit(self):
+ self.make_bundle_using_old_serial_batch_fields()
self.update_stock_ledger()
self.make_gl_entries()
self.update_target_asset()
diff --git a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json
index 26e1c3c270f..8eda441781f 100644
--- a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json
+++ b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json
@@ -18,9 +18,12 @@
"amount",
"batch_and_serial_no_section",
"serial_and_batch_bundle",
+ "use_serial_batch_fields",
"column_break_13",
- "batch_no",
+ "section_break_bfqc",
"serial_no",
+ "column_break_mbuv",
+ "batch_no",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break"
@@ -39,13 +42,13 @@
"reqd": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
"no_copy": 1,
"options": "Batch",
- "print_hide": 1,
- "read_only": 1
+ "print_hide": 1
},
{
"fieldname": "section_break_6",
@@ -102,12 +105,12 @@
"fieldtype": "Column Break"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_no",
"fieldtype": "Small Text",
"hidden": 1,
"label": "Serial No",
- "print_hide": 1,
- "read_only": 1
+ "print_hide": 1
},
{
"fieldname": "item_code",
@@ -148,18 +151,34 @@
"fieldtype": "Column Break"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "use_serial_batch_fields",
+ "fieldtype": "Check",
+ "label": "Use Serial No / Batch Fields"
+ },
+ {
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
+ "fieldname": "section_break_bfqc",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_mbuv",
+ "fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2023-04-06 01:10:17.947952",
+ "modified": "2024-02-04 16:41:09.239762",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Capitalization Stock Item",
diff --git a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py
index 122cbb600d6..d2b075c3e68 100644
--- a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py
+++ b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py
@@ -27,6 +27,7 @@ class AssetCapitalizationStockItem(Document):
serial_no: DF.SmallText | None
stock_qty: DF.Float
stock_uom: DF.Link
+ use_serial_batch_fields: DF.Check
valuation_rate: DF.Currency
warehouse: DF.Link
# end: auto-generated types
diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json
index ddcbd555ae8..ae854f29343 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.json
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -1,7 +1,6 @@
{
"actions": [],
"creation": "2013-06-25 11:04:03",
- "description": "Settings for Buying Module",
"doctype": "DocType",
"document_type": "Other",
"engine": "InnoDB",
@@ -152,6 +151,7 @@
},
{
"default": "1",
+ "depends_on": "eval: frappe.boot.versions && frappe.boot.versions.payments",
"fieldname": "show_pay_button",
"fieldtype": "Check",
"label": "Show Pay Button in Purchase Order Portal"
@@ -214,7 +214,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2024-01-12 16:42:01.894346",
+ "modified": "2024-01-31 13:34:18.101256",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",
@@ -264,4 +264,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index a28a310306f..9ca20a9ba2f 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -453,6 +453,7 @@ class PurchaseOrder(BuyingController):
self.update_ordered_qty()
self.update_reserved_qty_for_subcontract()
self.update_subcontracting_order_status()
+ self.update_blanket_order()
self.notify_update()
clear_doctype_notifications(self)
@@ -626,6 +627,7 @@ class PurchaseOrder(BuyingController):
update_sco_status(sco, "Closed" if self.status == "Closed" else None)
+@frappe.request_cache
def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0):
"""get last purchase rate for an item"""
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index f80a00a95f6..1216fb92658 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -814,6 +814,30 @@ class TestPurchaseOrder(FrappeTestCase):
# To test if the PO does NOT have a Blanket Order
self.assertEqual(po_doc.items[0].blanket_order, None)
+ def test_blanket_order_on_po_close_and_open(self):
+ # Step - 1: Create Blanket Order
+ bo = make_blanket_order(blanket_order_type="Purchasing", quantity=10, rate=10)
+
+ # Step - 2: Create Purchase Order
+ po = create_purchase_order(
+ item_code="_Test Item", qty=5, against_blanket_order=1, against_blanket=bo.name
+ )
+
+ bo.load_from_db()
+ self.assertEqual(bo.items[0].ordered_qty, 5)
+
+ # Step - 3: Close Purchase Order
+ po.update_status("Closed")
+
+ bo.load_from_db()
+ self.assertEqual(bo.items[0].ordered_qty, 0)
+
+ # Step - 4: Re-Open Purchase Order
+ po.update_status("Re-open")
+
+ bo.load_from_db()
+ self.assertEqual(bo.items[0].ordered_qty, 5)
+
def test_payment_terms_are_fetched_when_creating_purchase_invoice(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
create_payment_terms_template,
@@ -1113,6 +1137,7 @@ def create_purchase_order(**args):
"schedule_date": add_days(nowdate(), 1),
"include_exploded_items": args.get("include_exploded_items", 1),
"against_blanket_order": args.against_blanket_order,
+ "against_blanket": args.against_blanket,
"material_request": args.material_request,
"material_request_item": args.material_request_item,
},
diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
index 5a24cc2e92d..e3e8def7ffd 100644
--- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
+++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
@@ -545,7 +545,6 @@
"fieldname": "blanket_order",
"fieldtype": "Link",
"label": "Blanket Order",
- "no_copy": 1,
"options": "Blanket Order"
},
{
@@ -553,7 +552,6 @@
"fieldname": "blanket_order_rate",
"fieldtype": "Currency",
"label": "Blanket Order Rate",
- "no_copy": 1,
"print_hide": 1,
"read_only": 1
},
@@ -917,7 +915,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2023-11-24 13:24:41.298416",
+ "modified": "2024-02-05 11:23:24.859435",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 03c3cd2f9df..81a7a101e5e 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -693,7 +693,7 @@ class AccountsController(TransactionBase):
if self.get("is_subcontracted"):
args["is_subcontracted"] = self.is_subcontracted
- ret = get_item_details(args, self, for_validate=True, overwrite_warehouse=False)
+ ret = get_item_details(args, self, for_validate=for_validate, overwrite_warehouse=False)
for fieldname, value in ret.items():
if item.meta.get_field(fieldname) and value is not None:
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index 604a25127cf..9904c75e98a 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -724,17 +724,24 @@ def warehouse_query(doctype, txt, searchfield, start, page_len, filters):
conditions, bin_conditions = [], []
filter_dict = get_doctype_wise_filters(filters)
- query = """select `tabWarehouse`.name,
+ warehouse_field = "name"
+ meta = frappe.get_meta("Warehouse")
+ if meta.get("show_title_field_in_link") and meta.get("title_field"):
+ searchfield = meta.get("title_field")
+ warehouse_field = meta.get("title_field")
+
+ query = """select `tabWarehouse`.`{warehouse_field}`,
CONCAT_WS(' : ', 'Actual Qty', ifnull(round(`tabBin`.actual_qty, 2), 0 )) actual_qty
from `tabWarehouse` left join `tabBin`
on `tabBin`.warehouse = `tabWarehouse`.name {bin_conditions}
where
`tabWarehouse`.`{key}` like {txt}
{fcond} {mcond}
- order by ifnull(`tabBin`.actual_qty, 0) desc
+ order by ifnull(`tabBin`.actual_qty, 0) desc, `tabWarehouse`.`{warehouse_field}` asc
limit
{page_len} offset {start}
""".format(
+ warehouse_field=warehouse_field,
bin_conditions=get_filters_cond(
doctype, filter_dict.get("Bin"), bin_conditions, ignore_permissions=True
),
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 22b0d08c92a..0556c3c2a23 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -602,7 +602,7 @@ class SellingController(StockController):
if self.doctype in ["Sales Order", "Quotation"]:
for item in self.items:
item.gross_profit = flt(
- ((item.base_rate - item.valuation_rate) * item.stock_qty), self.precision("amount", item)
+ ((item.base_rate - flt(item.valuation_rate)) * item.stock_qty), self.precision("amount", item)
)
def set_customer_address(self):
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index c8516820ef3..ba3cdc8e833 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -21,6 +21,9 @@ from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
get_evaluated_inventory_dimension,
)
+from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
+ get_type_of_transaction,
+)
from erpnext.stock.stock_ledger import get_items_to_be_repost
@@ -126,6 +129,81 @@ class StockController(AccountsController):
# remove extra whitespace and store one serial no on each line
row.serial_no = clean_serial_no_string(row.serial_no)
+ def make_bundle_using_old_serial_batch_fields(self):
+ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+ from erpnext.stock.serial_batch_bundle import SerialBatchCreation
+
+ # To handle test cases
+ if frappe.flags.in_test and frappe.flags.use_serial_and_batch_fields:
+ return
+
+ table_name = "items"
+ if self.doctype == "Asset Capitalization":
+ table_name = "stock_items"
+
+ for row in self.get(table_name):
+ if not row.serial_no and not row.batch_no and not row.get("rejected_serial_no"):
+ continue
+
+ if not row.use_serial_batch_fields and (
+ row.serial_no or row.batch_no or row.get("rejected_serial_no")
+ ):
+ frappe.throw(_("Please enable Use Old Serial / Batch Fields to make_bundle"))
+
+ if row.use_serial_batch_fields and (
+ not row.serial_and_batch_bundle and not row.get("rejected_serial_and_batch_bundle")
+ ):
+ if self.doctype == "Stock Reconciliation":
+ qty = row.qty
+ type_of_transaction = "Inward"
+ else:
+ qty = row.stock_qty
+ type_of_transaction = get_type_of_transaction(self, row)
+
+ sn_doc = SerialBatchCreation(
+ {
+ "item_code": row.item_code,
+ "warehouse": row.warehouse,
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "voucher_detail_no": row.name,
+ "qty": qty,
+ "type_of_transaction": type_of_transaction,
+ "company": self.company,
+ "is_rejected": 1 if row.get("rejected_warehouse") else 0,
+ "serial_nos": get_serial_nos(row.serial_no) if row.serial_no else None,
+ "batches": frappe._dict({row.batch_no: qty}) if row.batch_no else None,
+ "batch_no": row.batch_no,
+ "use_serial_batch_fields": row.use_serial_batch_fields,
+ "do_not_submit": True,
+ }
+ ).make_serial_and_batch_bundle()
+
+ if sn_doc.is_rejected:
+ row.rejected_serial_and_batch_bundle = sn_doc.name
+ row.db_set(
+ {
+ "rejected_serial_and_batch_bundle": sn_doc.name,
+ "rejected_serial_no": "",
+ }
+ )
+ else:
+ row.serial_and_batch_bundle = sn_doc.name
+ row.db_set(
+ {
+ "serial_and_batch_bundle": sn_doc.name,
+ "serial_no": "",
+ "batch_no": "",
+ }
+ )
+
+ def set_use_serial_batch_fields(self):
+ if frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields"):
+ for row in self.items:
+ row.use_serial_batch_fields = 1
+
def get_gl_entries(
self, warehouse_account=None, default_expense_account=None, default_cost_center=None
):
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index f9f68a119b3..de942b7474f 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -97,6 +97,7 @@ class calculate_taxes_and_totals(object):
item_doc = frappe.get_cached_doc("Item", item.item_code)
args = {
"net_rate": item.net_rate or item.rate,
+ "base_net_rate": item.base_net_rate or item.base_rate,
"tax_category": self.doc.get("tax_category"),
"posting_date": self.doc.get("posting_date"),
"bill_date": self.doc.get("bill_date"),
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 23650b68736..079350b63b2 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -955,6 +955,14 @@ class JobCard(Document):
if update_status:
self.db_set("status", self.status)
+ if self.status in ["Completed", "Work In Progress"]:
+ status = {
+ "Completed": "Off",
+ "Work In Progress": "Production",
+ }.get(self.status)
+
+ self.update_status_in_workstation(status)
+
def set_wip_warehouse(self):
if not self.wip_warehouse:
self.wip_warehouse = frappe.db.get_single_value(
@@ -1035,6 +1043,12 @@ class JobCard(Document):
return False
+ def update_status_in_workstation(self, status):
+ if not self.workstation:
+ return
+
+ frappe.db.set_value("Workstation", self.workstation, "status", status)
+
@frappe.whitelist()
def make_time_log(args):
diff --git a/erpnext/manufacturing/doctype/plant_floor/__init__.py b/erpnext/manufacturing/doctype/plant_floor/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/manufacturing/doctype/plant_floor/plant_floor.js b/erpnext/manufacturing/doctype/plant_floor/plant_floor.js
new file mode 100644
index 00000000000..67e5acd9da8
--- /dev/null
+++ b/erpnext/manufacturing/doctype/plant_floor/plant_floor.js
@@ -0,0 +1,256 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Plant Floor", {
+ setup(frm) {
+ frm.trigger("setup_queries");
+ },
+
+ setup_queries(frm) {
+ frm.set_query("warehouse", (doc) => {
+ if (!doc.company) {
+ frappe.throw(__("Please select Company first"));
+ }
+
+ return {
+ filters: {
+ "is_group": 0,
+ "company": doc.company
+ }
+ }
+ });
+ },
+
+ refresh(frm) {
+ frm.trigger('prepare_stock_dashboard')
+ frm.trigger('prepare_workstation_dashboard')
+ },
+
+ prepare_workstation_dashboard(frm) {
+ let wrapper = $(frm.fields_dict["plant_dashboard"].wrapper);
+ wrapper.empty();
+
+ frappe.visual_plant_floor = new frappe.ui.VisualPlantFloor({
+ wrapper: wrapper,
+ skip_filters: true,
+ plant_floor: frm.doc.name,
+ });
+ },
+
+ prepare_stock_dashboard(frm) {
+ if (!frm.doc.warehouse) {
+ return;
+ }
+
+ let wrapper = $(frm.fields_dict["stock_summary"].wrapper);
+ wrapper.empty();
+
+ frappe.visual_stock = new VisualStock({
+ wrapper: wrapper,
+ frm: frm,
+ });
+ },
+});
+
+
+class VisualStock {
+ constructor(opts) {
+ Object.assign(this, opts);
+ this.make();
+ }
+
+ make() {
+ this.prepare_filters();
+ this.prepare_stock_summary({
+ start:0
+ });
+ }
+
+ prepare_filters() {
+ this.wrapper.append(`
+
+ `);
+
+ this.item_filter = frappe.ui.form.make_control({
+ df: {
+ fieldtype: "Link",
+ fieldname: "item_code",
+ placeholder: __("Item"),
+ options: "Item",
+ onchange: () => this.prepare_stock_summary({
+ start:0,
+ item_code: this.item_filter.value
+ })
+ },
+ parent: this.wrapper.find('.filter-section'),
+ render_input: true,
+ });
+
+ this.item_filter.$wrapper.addClass('form-column col-sm-3');
+ this.item_filter.$wrapper.find('.clearfix').hide();
+
+ this.item_group_filter = frappe.ui.form.make_control({
+ df: {
+ fieldtype: "Link",
+ fieldname: "item_group",
+ placeholder: __("Item Group"),
+ options: "Item Group",
+ change: () => this.prepare_stock_summary({
+ start:0,
+ item_group: this.item_group_filter.value
+ })
+ },
+ parent: this.wrapper.find('.filter-section'),
+ render_input: true,
+ });
+
+ this.item_group_filter.$wrapper.addClass('form-column col-sm-3');
+ this.item_group_filter.$wrapper.find('.clearfix').hide();
+ }
+
+ prepare_stock_summary(args) {
+ let {start, item_code, item_group} = args;
+
+ this.get_stock_summary(start, item_code, item_group).then(stock_summary => {
+ this.wrapper.find('.stock-summary-container').remove();
+ this.wrapper.append(``);
+ this.stock_summary = stock_summary.message;
+ this.render_stock_summary();
+ this.bind_events();
+ });
+ }
+
+ async get_stock_summary(start, item_code, item_group) {
+ let stock_summary = await frappe.call({
+ method: "erpnext.manufacturing.doctype.plant_floor.plant_floor.get_stock_summary",
+ args: {
+ warehouse: this.frm.doc.warehouse,
+ start: start,
+ item_code: item_code,
+ item_group: item_group
+ }
+ });
+
+ return stock_summary;
+ }
+
+ render_stock_summary() {
+ let template = frappe.render_template("stock_summary_template", {
+ stock_summary: this.stock_summary
+ });
+
+ this.wrapper.find('.stock-summary-container').append(template);
+ }
+
+ bind_events() {
+ this.wrapper.find('.btn-add').click((e) => {
+ this.item_code = decodeURI($(e.currentTarget).attr('data-item-code'));
+
+ this.make_stock_entry([
+ {
+ label: __("For Item"),
+ fieldname: "item_code",
+ fieldtype: "Data",
+ read_only: 1,
+ default: this.item_code
+ },
+ {
+ label: __("Quantity"),
+ fieldname: "qty",
+ fieldtype: "Float",
+ reqd: 1
+ }
+ ], __("Add Stock"), "Material Receipt")
+ });
+
+ this.wrapper.find('.btn-move').click((e) => {
+ this.item_code = decodeURI($(e.currentTarget).attr('data-item-code'));
+
+ this.make_stock_entry([
+ {
+ label: __("For Item"),
+ fieldname: "item_code",
+ fieldtype: "Data",
+ read_only: 1,
+ default: this.item_code
+ },
+ {
+ label: __("Quantity"),
+ fieldname: "qty",
+ fieldtype: "Float",
+ reqd: 1
+ },
+ {
+ label: __("To Warehouse"),
+ fieldname: "to_warehouse",
+ fieldtype: "Link",
+ options: "Warehouse",
+ reqd: 1,
+ get_query: () => {
+ return {
+ filters: {
+ "is_group": 0,
+ "company": this.frm.doc.company
+ }
+ }
+ }
+ }
+ ], __("Move Stock"), "Material Transfer")
+ });
+ }
+
+ make_stock_entry(fields, title, stock_entry_type) {
+ frappe.prompt(fields,
+ (values) => {
+ this.values = values;
+ this.stock_entry_type = stock_entry_type;
+ this.update_values();
+
+ this.frm.call({
+ method: "make_stock_entry",
+ doc: this.frm.doc,
+ args: {
+ kwargs: this.values,
+ },
+ callback: (r) => {
+ if (!r.exc) {
+ var doc = frappe.model.sync(r.message);
+ frappe.set_route("Form", r.message.doctype, r.message.name);
+ }
+ }
+ })
+ }, __(title), __("Create")
+ );
+ }
+
+ update_values() {
+ if (!this.values.qty) {
+ frappe.throw(__("Quantity is required"));
+ }
+
+ let from_warehouse = "";
+ let to_warehouse = "";
+
+ if (this.stock_entry_type == "Material Receipt") {
+ to_warehouse = this.frm.doc.warehouse;
+ } else {
+ from_warehouse = this.frm.doc.warehouse;
+ to_warehouse = this.values.to_warehouse;
+ }
+
+ this.values = {
+ ...this.values,
+ ...{
+ "company": this.frm.doc.company,
+ "item_code": this.item_code,
+ "from_warehouse": from_warehouse,
+ "to_warehouse": to_warehouse,
+ "purpose": this.stock_entry_type,
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/plant_floor/plant_floor.json b/erpnext/manufacturing/doctype/plant_floor/plant_floor.json
new file mode 100644
index 00000000000..be0052c47bf
--- /dev/null
+++ b/erpnext/manufacturing/doctype/plant_floor/plant_floor.json
@@ -0,0 +1,97 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "field:floor_name",
+ "creation": "2023-10-06 15:06:07.976066",
+ "default_view": "List",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "workstations_tab",
+ "plant_dashboard",
+ "stock_summary_tab",
+ "stock_summary",
+ "details_tab",
+ "column_break_mvbx",
+ "floor_name",
+ "company",
+ "warehouse"
+ ],
+ "fields": [
+ {
+ "fieldname": "floor_name",
+ "fieldtype": "Data",
+ "label": "Floor Name",
+ "unique": 1
+ },
+ {
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "workstations_tab",
+ "fieldtype": "Tab Break",
+ "label": "Workstations"
+ },
+ {
+ "fieldname": "plant_dashboard",
+ "fieldtype": "HTML",
+ "label": "Plant Dashboard"
+ },
+ {
+ "fieldname": "details_tab",
+ "fieldtype": "Tab Break",
+ "label": "Floor"
+ },
+ {
+ "fieldname": "column_break_mvbx",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "label": "Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "depends_on": "eval:!doc.__islocal && doc.warehouse",
+ "fieldname": "stock_summary_tab",
+ "fieldtype": "Tab Break",
+ "label": "Stock Summary"
+ },
+ {
+ "fieldname": "stock_summary",
+ "fieldtype": "HTML",
+ "label": "Stock Summary"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2024-01-30 11:59:07.508535",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Plant Floor",
+ "naming_rule": "By fieldname",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/plant_floor/plant_floor.py b/erpnext/manufacturing/doctype/plant_floor/plant_floor.py
new file mode 100644
index 00000000000..d30b7d10499
--- /dev/null
+++ b/erpnext/manufacturing/doctype/plant_floor/plant_floor.py
@@ -0,0 +1,129 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe.model.document import Document
+from frappe.query_builder import Order
+from frappe.utils import get_link_to_form, nowdate, nowtime
+
+
+class PlantFloor(Document):
+ # begin: auto-generated types
+ # This code is auto-generated. Do not modify anything in this block.
+
+ from typing import TYPE_CHECKING
+
+ if TYPE_CHECKING:
+ from frappe.types import DF
+
+ company: DF.Link | None
+ floor_name: DF.Data | None
+ warehouse: DF.Link | None
+ # end: auto-generated types
+
+ @frappe.whitelist()
+ def make_stock_entry(self, kwargs):
+ if isinstance(kwargs, str):
+ kwargs = frappe.parse_json(kwargs)
+
+ if isinstance(kwargs, dict):
+ kwargs = frappe._dict(kwargs)
+
+ stock_entry = frappe.new_doc("Stock Entry")
+ stock_entry.update(
+ {
+ "company": kwargs.company,
+ "from_warehouse": kwargs.from_warehouse,
+ "to_warehouse": kwargs.to_warehouse,
+ "purpose": kwargs.purpose,
+ "stock_entry_type": kwargs.purpose,
+ "posting_date": nowdate(),
+ "posting_time": nowtime(),
+ "items": self.get_item_details(kwargs),
+ }
+ )
+
+ stock_entry.set_missing_values()
+
+ return stock_entry
+
+ def get_item_details(self, kwargs) -> list[dict]:
+ item_details = frappe.db.get_value(
+ "Item", kwargs.item_code, ["item_name", "stock_uom", "item_group", "description"], as_dict=True
+ )
+ item_details.update(
+ {
+ "qty": kwargs.qty,
+ "uom": item_details.stock_uom,
+ "item_code": kwargs.item_code,
+ "conversion_factor": 1,
+ "s_warehouse": kwargs.from_warehouse,
+ "t_warehouse": kwargs.to_warehouse,
+ }
+ )
+
+ return [item_details]
+
+
+@frappe.whitelist()
+def get_stock_summary(warehouse, start=0, item_code=None, item_group=None):
+ stock_details = get_stock_details(
+ warehouse, start=start, item_code=item_code, item_group=item_group
+ )
+
+ max_count = 0.0
+ for d in stock_details:
+ d.actual_or_pending = (
+ d.projected_qty
+ + d.reserved_qty
+ + d.reserved_qty_for_production
+ + d.reserved_qty_for_sub_contract
+ )
+ d.pending_qty = 0
+ d.total_reserved = (
+ d.reserved_qty + d.reserved_qty_for_production + d.reserved_qty_for_sub_contract
+ )
+ if d.actual_or_pending > d.actual_qty:
+ d.pending_qty = d.actual_or_pending - d.actual_qty
+
+ d.max_count = max(d.actual_or_pending, d.actual_qty, d.total_reserved, max_count)
+ max_count = d.max_count
+ d.item_link = get_link_to_form("Item", d.item_code)
+
+ return stock_details
+
+
+def get_stock_details(warehouse, start=0, item_code=None, item_group=None):
+ item_table = frappe.qb.DocType("Item")
+ bin_table = frappe.qb.DocType("Bin")
+
+ query = (
+ frappe.qb.from_(bin_table)
+ .inner_join(item_table)
+ .on(bin_table.item_code == item_table.name)
+ .select(
+ bin_table.item_code,
+ bin_table.actual_qty,
+ bin_table.projected_qty,
+ bin_table.reserved_qty,
+ bin_table.reserved_qty_for_production,
+ bin_table.reserved_qty_for_sub_contract,
+ bin_table.reserved_qty_for_production_plan,
+ bin_table.reserved_stock,
+ item_table.item_name,
+ item_table.item_group,
+ item_table.image,
+ )
+ .where(bin_table.warehouse == warehouse)
+ .limit(20)
+ .offset(start)
+ .orderby(bin_table.actual_qty, order=Order.desc)
+ )
+
+ if item_code:
+ query = query.where(bin_table.item_code == item_code)
+
+ if item_group:
+ query = query.where(item_table.item_group == item_group)
+
+ return query.run(as_dict=True)
diff --git a/erpnext/manufacturing/doctype/plant_floor/stock_summary_template.html b/erpnext/manufacturing/doctype/plant_floor/stock_summary_template.html
new file mode 100644
index 00000000000..8824c9811ba
--- /dev/null
+++ b/erpnext/manufacturing/doctype/plant_floor/stock_summary_template.html
@@ -0,0 +1,61 @@
+{% $.each(stock_summary, (idx, row) => { %}
+
+
+ {% if(row.image) { %}
+

+ {% } else { %}
+
{{frappe.get_abbr(row.item_code, 2)}}
+ {% } %}
+
+
+ {% if (row.item_code === row.item_name) { %}
+ {{row.item_link}}
+ {% } else { %}
+ {{row.item_link}}
+
+ {{row.item_name}}
+
+ {% } %}
+
+
+
+ {{ frappe.format(row.actual_qty, { fieldtype: "Float"})}}
+
+
+ {{ frappe.format(row.reserved_stock, { fieldtype: "Float"})}}
+
+
+
+
+ {{ row.total_reserved }}
+
+
+
+
+
+
+
+ {{ row.actual_qty }} {{ (row.pending_qty > 0) ? ("(" + row.pending_qty+ ")") : "" }}
+
+
+
+
+ {% if row.pending_qty > 0 %}
+
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+{% }); %}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/plant_floor/test_plant_floor.py b/erpnext/manufacturing/doctype/plant_floor/test_plant_floor.py
new file mode 100644
index 00000000000..2fac2113366
--- /dev/null
+++ b/erpnext/manufacturing/doctype/plant_floor/test_plant_floor.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestPlantFloor(FrappeTestCase):
+ pass
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 51658a03a7c..d460108d7b4 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -1334,10 +1334,10 @@ def get_sales_orders(self):
)
date_field_mapper = {
- "from_date": self.from_date >= so.transaction_date,
- "to_date": self.to_date <= so.transaction_date,
- "from_delivery_date": self.from_delivery_date >= so_item.delivery_date,
- "to_delivery_date": self.to_delivery_date <= so_item.delivery_date,
+ "from_date": so.transaction_date >= self.from_date,
+ "to_date": so.transaction_date <= self.to_date,
+ "from_delivery_date": so_item.delivery_date >= self.from_delivery_date,
+ "to_delivery_date": so_item.delivery_date <= self.to_delivery_date,
}
for field, value in date_field_mapper.items():
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 78bfc767d6f..02a1d27432c 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -1511,14 +1511,14 @@ def get_serial_nos_for_work_order(work_order, production_item):
def validate_operation_data(row):
- if row.get("qty") <= 0:
+ if flt(row.get("qty")) <= 0:
frappe.throw(
_("Quantity to Manufacture can not be zero for the operation {0}").format(
frappe.bold(row.get("operation"))
)
)
- if row.get("qty") > row.get("pending_qty"):
+ if flt(row.get("qty")) > flt(row.get("pending_qty")):
frappe.throw(
_("For operation {0}: Quantity ({1}) can not be greter than pending quantity({2})").format(
frappe.bold(row.get("operation")),
diff --git a/erpnext/manufacturing/doctype/workstation/workstation.js b/erpnext/manufacturing/doctype/workstation/workstation.js
index f830b170ed0..e3ad3fe3cce 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation.js
+++ b/erpnext/manufacturing/doctype/workstation/workstation.js
@@ -2,6 +2,28 @@
// License: GNU General Public License v3. See license.txt
frappe.ui.form.on("Workstation", {
+ set_illustration_image(frm) {
+ let status_image_field = frm.doc.status == "Production" ? frm.doc.on_status_image : frm.doc.off_status_image;
+ if (status_image_field) {
+ frm.sidebar.image_wrapper.find(".sidebar-image").attr("src", status_image_field);
+ }
+ },
+
+ refresh(frm) {
+ frm.trigger("set_illustration_image");
+ frm.trigger("prepapre_dashboard");
+ },
+
+ prepapre_dashboard(frm) {
+ let $parent = $(frm.fields_dict["workstation_dashboard"].wrapper);
+ $parent.empty();
+
+ let workstation_dashboard = new WorkstationDashboard({
+ wrapper: $parent,
+ frm: frm
+ });
+ },
+
onload(frm) {
if(frm.is_new())
{
@@ -54,3 +76,243 @@ frappe.tour['Workstation'] = [
];
+
+
+class WorkstationDashboard {
+ constructor({ wrapper, frm }) {
+ this.$wrapper = $(wrapper);
+ this.frm = frm;
+
+ this.prepapre_dashboard();
+ }
+
+ prepapre_dashboard() {
+ frappe.call({
+ method: "erpnext.manufacturing.doctype.workstation.workstation.get_job_cards",
+ args: {
+ workstation: this.frm.doc.name
+ },
+ callback: (r) => {
+ if (r.message) {
+ this.job_cards = r.message;
+ this.render_job_cards();
+ }
+ }
+ });
+ }
+
+ render_job_cards() {
+ let template = frappe.render_template("workstation_job_card", {
+ data: this.job_cards
+ });
+
+ this.$wrapper.html(template);
+ this.prepare_timer();
+ this.toggle_job_card();
+ this.bind_events();
+ }
+
+ toggle_job_card() {
+ this.$wrapper.find(".collapse-indicator-job").on("click", (e) => {
+ $(e.currentTarget).closest(".form-dashboard-section").find(".section-body-job-card").toggleClass("hide")
+ if ($(e.currentTarget).closest(".form-dashboard-section").find(".section-body-job-card").hasClass("hide"))
+ $(e.currentTarget).html(frappe.utils.icon("es-line-down", "sm", "mb-1"))
+ else
+ $(e.currentTarget).html(frappe.utils.icon("es-line-up", "sm", "mb-1"))
+ });
+ }
+
+ bind_events() {
+ this.$wrapper.find(".make-material-request").on("click", (e) => {
+ let job_card = $(e.currentTarget).attr("job-card");
+ this.make_material_request(job_card);
+ });
+
+ this.$wrapper.find(".btn-start").on("click", (e) => {
+ let job_card = $(e.currentTarget).attr("job-card");
+ this.start_job(job_card);
+ });
+
+ this.$wrapper.find(".btn-complete").on("click", (e) => {
+ let job_card = $(e.currentTarget).attr("job-card");
+ let pending_qty = flt($(e.currentTarget).attr("pending-qty"));
+ this.complete_job(job_card, pending_qty);
+ });
+ }
+
+ start_job(job_card) {
+ let me = this;
+ frappe.prompt([
+ {
+ fieldtype: 'Datetime',
+ label: __('Start Time'),
+ fieldname: 'start_time',
+ reqd: 1,
+ default: frappe.datetime.now_datetime()
+ },
+ {
+ label: __('Operator'),
+ fieldname: 'employee',
+ fieldtype: 'Link',
+ options: 'Employee',
+ }
+ ], data => {
+ this.frm.call({
+ method: "start_job",
+ doc: this.frm.doc,
+ args: {
+ job_card: job_card,
+ from_time: data.start_time,
+ employee: data.employee,
+ },
+ callback(r) {
+ if (r.message) {
+ me.job_cards = [r.message];
+ me.prepare_timer()
+ me.update_job_card_details();
+ }
+ }
+ });
+ }, __("Enter Value"), __("Start Job"));
+ }
+
+ complete_job(job_card, qty_to_manufacture) {
+ let me = this;
+ let fields = [
+ {
+ fieldtype: 'Float',
+ label: __('Completed Quantity'),
+ fieldname: 'qty',
+ reqd: 1,
+ default: flt(qty_to_manufacture || 0)
+ },
+ {
+ fieldtype: 'Datetime',
+ label: __('End Time'),
+ fieldname: 'end_time',
+ default: frappe.datetime.now_datetime()
+ },
+ ];
+
+ frappe.prompt(fields, data => {
+ if (data.qty <= 0) {
+ frappe.throw(__("Quantity should be greater than 0"));
+ }
+
+ this.frm.call({
+ method: "complete_job",
+ doc: this.frm.doc,
+ args: {
+ job_card: job_card,
+ qty: data.qty,
+ to_time: data.end_time,
+ },
+ callback: function(r) {
+ if (r.message) {
+ me.job_cards = [r.message];
+ me.prepare_timer()
+ me.update_job_card_details();
+ }
+ }
+ });
+ }, __("Enter Value"), __("Submit"));
+ }
+
+ make_material_request(job_card) {
+ frappe.call({
+ method: "erpnext.manufacturing.doctype.job_card.job_card.make_material_request",
+ args: {
+ source_name: job_card,
+ },
+ callback: (r) => {
+ if (r.message) {
+ var doc = frappe.model.sync(r.message)[0];
+ frappe.set_route("Form", doc.doctype, doc.name);
+ }
+ }
+ });
+ }
+
+ prepare_timer() {
+ this.job_cards.forEach((data) => {
+ if (data.time_logs?.length) {
+ data._current_time = this.get_current_time(data);
+ if (data.time_logs[cint(data.time_logs.length) - 1].to_time) {
+ this.updateStopwatch(data);
+ } else {
+ this.initialiseTimer(data);
+ }
+ }
+ });
+ }
+
+ update_job_card_details() {
+ let color_map = {
+ "Pending": "var(--bg-blue)",
+ "In Process": "var(--bg-yellow)",
+ "Submitted": "var(--bg-blue)",
+ "Open": "var(--bg-gray)",
+ "Closed": "var(--bg-green)",
+ "Work In Progress": "var(--bg-orange)",
+ }
+
+ this.job_cards.forEach((data) => {
+ let job_card_selector = this.$wrapper.find(`
+ [data-name='${data.name}']`
+ );
+
+ $(job_card_selector).find(".job-card-status").text(data.status);
+ $(job_card_selector).find(".job-card-status").css("backgroundColor", color_map[data.status]);
+
+ if (data.status === "Work In Progress") {
+ $(job_card_selector).find(".btn-start").addClass("hide");
+ $(job_card_selector).find(".btn-complete").removeClass("hide");
+ } else if (data.status === "Completed") {
+ $(job_card_selector).find(".btn-start").addClass("hide");
+ $(job_card_selector).find(".btn-complete").addClass("hide");
+ }
+ });
+ }
+
+ initialiseTimer(data) {
+ setInterval(() => {
+ data._current_time += 1;
+ this.updateStopwatch(data);
+ }, 1000);
+ }
+
+ updateStopwatch(data) {
+ let increment = data._current_time;
+ let hours = Math.floor(increment / 3600);
+ let minutes = Math.floor((increment - (hours * 3600)) / 60);
+ let seconds = cint(increment - (hours * 3600) - (minutes * 60));
+
+ let job_card_selector = `[data-job-card='${data.name}']`
+ let timer_selector = this.$wrapper.find(job_card_selector)
+
+ $(timer_selector).find(".hours").text(hours < 10 ? ("0" + hours.toString()) : hours.toString());
+ $(timer_selector).find(".minutes").text(minutes < 10 ? ("0" + minutes.toString()) : minutes.toString());
+ $(timer_selector).find(".seconds").text(seconds < 10 ? ("0" + seconds.toString()) : seconds.toString());
+ }
+
+ get_current_time(data) {
+ let current_time = 0.0;
+ data.time_logs.forEach(d => {
+ if (d.to_time) {
+ if (d.time_in_mins) {
+ current_time += flt(d.time_in_mins, 2) * 60;
+ } else {
+ current_time += this.get_seconds_diff(d.to_time, d.from_time);
+ }
+ } else {
+ current_time += this.get_seconds_diff(frappe.datetime.now_datetime(), d.from_time);
+ }
+ });
+
+ return current_time;
+ }
+
+ get_seconds_diff(d1, d2) {
+ return moment(d1).diff(d2, "seconds");
+ }
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/workstation/workstation.json b/erpnext/manufacturing/doctype/workstation/workstation.json
index 881cba0cce0..5912714052b 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation.json
+++ b/erpnext/manufacturing/doctype/workstation/workstation.json
@@ -8,10 +8,24 @@
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
+ "dashboard_tab",
+ "workstation_dashboard",
+ "details_tab",
"workstation_name",
- "production_capacity",
- "column_break_3",
"workstation_type",
+ "plant_floor",
+ "column_break_3",
+ "production_capacity",
+ "warehouse",
+ "production_capacity_section",
+ "parts_per_hour",
+ "workstation_status_tab",
+ "status",
+ "column_break_glcv",
+ "illustration_section",
+ "on_status_image",
+ "column_break_etmc",
+ "off_status_image",
"over_heads",
"hour_rate_electricity",
"hour_rate_consumable",
@@ -24,7 +38,9 @@
"description",
"working_hours_section",
"holiday_list",
- "working_hours"
+ "working_hours",
+ "total_working_hours",
+ "connections_tab"
],
"fields": [
{
@@ -120,9 +136,10 @@
},
{
"default": "1",
+ "description": "Run parallel job cards in a workstation",
"fieldname": "production_capacity",
"fieldtype": "Int",
- "label": "Production Capacity",
+ "label": "Job Capacity",
"reqd": 1
},
{
@@ -145,12 +162,97 @@
{
"fieldname": "section_break_11",
"fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "plant_floor",
+ "fieldtype": "Link",
+ "label": "Plant Floor",
+ "options": "Plant Floor"
+ },
+ {
+ "fieldname": "workstation_status_tab",
+ "fieldtype": "Tab Break",
+ "label": "Workstation Status"
+ },
+ {
+ "fieldname": "illustration_section",
+ "fieldtype": "Section Break",
+ "label": "Status Illustration"
+ },
+ {
+ "fieldname": "column_break_etmc",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Status",
+ "options": "Production\nOff\nIdle\nProblem\nMaintenance\nSetup"
+ },
+ {
+ "fieldname": "column_break_glcv",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "on_status_image",
+ "fieldtype": "Attach Image",
+ "label": "Active Status"
+ },
+ {
+ "fieldname": "off_status_image",
+ "fieldtype": "Attach Image",
+ "label": "Inactive Status"
+ },
+ {
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "label": "Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "fieldname": "production_capacity_section",
+ "fieldtype": "Section Break",
+ "label": "Production Capacity"
+ },
+ {
+ "fieldname": "parts_per_hour",
+ "fieldtype": "Float",
+ "label": "Parts Per Hour"
+ },
+ {
+ "fieldname": "total_working_hours",
+ "fieldtype": "Float",
+ "label": "Total Working Hours"
+ },
+ {
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "dashboard_tab",
+ "fieldtype": "Tab Break",
+ "label": "Job Cards"
+ },
+ {
+ "fieldname": "details_tab",
+ "fieldtype": "Tab Break",
+ "label": "Details"
+ },
+ {
+ "fieldname": "connections_tab",
+ "fieldtype": "Tab Break",
+ "label": "Connections",
+ "show_dashboard": 1
+ },
+ {
+ "fieldname": "workstation_dashboard",
+ "fieldtype": "HTML",
+ "label": "Workstation Dashboard"
}
],
"icon": "icon-wrench",
"idx": 1,
+ "image_field": "on_status_image",
"links": [],
- "modified": "2022-11-04 17:39:01.549346",
+ "modified": "2023-11-30 12:43:35.808845",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Workstation",
diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py
index 0a247fc431d..90aa993d7e2 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation.py
+++ b/erpnext/manufacturing/doctype/workstation/workstation.py
@@ -11,7 +11,11 @@ from frappe.utils import (
comma_and,
flt,
formatdate,
+ get_link_to_form,
+ get_time,
+ get_url_to_form,
getdate,
+ time_diff_in_hours,
time_diff_in_seconds,
to_timedelta,
)
@@ -60,6 +64,23 @@ class Workstation(Document):
def before_save(self):
self.set_data_based_on_workstation_type()
self.set_hour_rate()
+ self.set_total_working_hours()
+
+ def set_total_working_hours(self):
+ self.total_working_hours = 0.0
+ for row in self.working_hours:
+ self.validate_working_hours(row)
+
+ if row.start_time and row.end_time:
+ row.hours = flt(time_diff_in_hours(row.end_time, row.start_time), row.precision("hours"))
+ self.total_working_hours += row.hours
+
+ def validate_working_hours(self, row):
+ if not (row.start_time and row.end_time):
+ frappe.throw(_("Row #{0}: Start Time and End Time are required").format(row.idx))
+
+ if get_time(row.start_time) >= get_time(row.end_time):
+ frappe.throw(_("Row #{0}: Start Time must be before End Time").format(row.idx))
def set_hour_rate(self):
self.hour_rate = (
@@ -143,6 +164,141 @@ class Workstation(Document):
return schedule_date
+ @frappe.whitelist()
+ def start_job(self, job_card, from_time, employee):
+ doc = frappe.get_doc("Job Card", job_card)
+ doc.append("time_logs", {"from_time": from_time, "employee": employee})
+ doc.save(ignore_permissions=True)
+
+ return doc
+
+ @frappe.whitelist()
+ def complete_job(self, job_card, qty, to_time):
+ doc = frappe.get_doc("Job Card", job_card)
+ for row in doc.time_logs:
+ if not row.to_time:
+ row.to_time = to_time
+ row.time_in_mins = time_diff_in_hours(row.to_time, row.from_time) / 60
+ row.completed_qty = qty
+
+ doc.save(ignore_permissions=True)
+ doc.submit()
+
+ return doc
+
+
+@frappe.whitelist()
+def get_job_cards(workstation):
+ if frappe.has_permission("Job Card", "read"):
+ jc_data = frappe.get_all(
+ "Job Card",
+ fields=[
+ "name",
+ "production_item",
+ "work_order",
+ "operation",
+ "total_completed_qty",
+ "for_quantity",
+ "transferred_qty",
+ "status",
+ "expected_start_date",
+ "expected_end_date",
+ "time_required",
+ "wip_warehouse",
+ ],
+ filters={
+ "workstation": workstation,
+ "docstatus": ("<", 2),
+ "status": ["not in", ["Completed", "Stopped"]],
+ },
+ order_by="expected_start_date, expected_end_date",
+ )
+
+ job_cards = [row.name for row in jc_data]
+ raw_materials = get_raw_materials(job_cards)
+ time_logs = get_time_logs(job_cards)
+
+ allow_excess_transfer = frappe.db.get_single_value(
+ "Manufacturing Settings", "job_card_excess_transfer"
+ )
+
+ for row in jc_data:
+ row.progress_percent = (
+ flt(row.total_completed_qty / row.for_quantity * 100, 2) if row.for_quantity else 0
+ )
+ row.progress_title = _("Total completed quantity: {0}").format(row.total_completed_qty)
+ row.status_color = get_status_color(row.status)
+ row.job_card_link = get_link_to_form("Job Card", row.name)
+ row.work_order_link = get_link_to_form("Work Order", row.work_order)
+
+ row.raw_materials = raw_materials.get(row.name, [])
+ row.time_logs = time_logs.get(row.name, [])
+ row.make_material_request = False
+ if row.for_quantity > row.transferred_qty or allow_excess_transfer:
+ row.make_material_request = True
+
+ return jc_data
+
+
+def get_status_color(status):
+ color_map = {
+ "Pending": "var(--bg-blue)",
+ "In Process": "var(--bg-yellow)",
+ "Submitted": "var(--bg-blue)",
+ "Open": "var(--bg-gray)",
+ "Closed": "var(--bg-green)",
+ "Work In Progress": "var(--bg-orange)",
+ }
+
+ return color_map.get(status, "var(--bg-blue)")
+
+
+def get_raw_materials(job_cards):
+ raw_materials = {}
+
+ data = frappe.get_all(
+ "Job Card Item",
+ fields=[
+ "parent",
+ "item_code",
+ "item_group",
+ "uom",
+ "item_name",
+ "source_warehouse",
+ "required_qty",
+ "transferred_qty",
+ ],
+ filters={"parent": ["in", job_cards]},
+ )
+
+ for row in data:
+ raw_materials.setdefault(row.parent, []).append(row)
+
+ return raw_materials
+
+
+def get_time_logs(job_cards):
+ time_logs = {}
+
+ data = frappe.get_all(
+ "Job Card Time Log",
+ fields=[
+ "parent",
+ "name",
+ "employee",
+ "from_time",
+ "to_time",
+ "time_in_mins",
+ ],
+ filters={"parent": ["in", job_cards], "parentfield": "time_logs"},
+ order_by="parent, idx",
+ )
+
+ for row in data:
+ time_logs.setdefault(row.parent, []).append(row)
+
+ return time_logs
+
@frappe.whitelist()
def get_default_holiday_list():
@@ -201,3 +357,52 @@ def check_workstation_for_holiday(workstation, from_datetime, to_datetime):
+ "\n".join(applicable_holidays),
WorkstationHolidayError,
)
+
+
+@frappe.whitelist()
+def get_workstations(**kwargs):
+ kwargs = frappe._dict(kwargs)
+ _workstation = frappe.qb.DocType("Workstation")
+
+ query = (
+ frappe.qb.from_(_workstation)
+ .select(
+ _workstation.name,
+ _workstation.description,
+ _workstation.status,
+ _workstation.on_status_image,
+ _workstation.off_status_image,
+ )
+ .orderby(_workstation.workstation_type, _workstation.name)
+ .where(_workstation.plant_floor == kwargs.plant_floor)
+ )
+
+ if kwargs.workstation:
+ query = query.where(_workstation.name == kwargs.workstation)
+
+ if kwargs.workstation_type:
+ query = query.where(_workstation.workstation_type == kwargs.workstation_type)
+
+ if kwargs.workstation_status:
+ query = query.where(_workstation.status == kwargs.workstation_status)
+
+ data = query.run(as_dict=True)
+
+ color_map = {
+ "Production": "var(--green-600)",
+ "Off": "var(--gray-600)",
+ "Idle": "var(--gray-600)",
+ "Problem": "var(--red-600)",
+ "Maintenance": "var(--yellow-600)",
+ "Setup": "var(--blue-600)",
+ }
+
+ for d in data:
+ d.workstation_name = get_link_to_form("Workstation", d.name)
+ d.status_image = d.on_status_image
+ d.background_color = color_map.get(d.status, "var(--red-600)")
+ d.workstation_link = get_url_to_form("Workstation", d.name)
+ if d.status != "Production":
+ d.status_image = d.off_status_image
+
+ return data
diff --git a/erpnext/manufacturing/doctype/workstation/workstation_job_card.html b/erpnext/manufacturing/doctype/workstation/workstation_job_card.html
new file mode 100644
index 00000000000..97707855db0
--- /dev/null
+++ b/erpnext/manufacturing/doctype/workstation/workstation_job_card.html
@@ -0,0 +1,125 @@
+
+
+
+{% $.each(data, (idx, d) => { %}
+
+{% }); %}
+
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/workstation/workstation_list.js b/erpnext/manufacturing/doctype/workstation/workstation_list.js
index 61f2062ec0b..86928cafcb2 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation_list.js
+++ b/erpnext/manufacturing/doctype/workstation/workstation_list.js
@@ -1,5 +1,16 @@
frappe.listview_settings['Workstation'] = {
- // add_fields: ["status"],
- // filters:[["status","=", "Open"]]
+ add_fields: ["status"],
+ get_indicator: function(doc) {
+ let color_map = {
+ "Production": "green",
+ "Off": "gray",
+ "Idle": "gray",
+ "Problem": "red",
+ "Maintenance": "yellow",
+ "Setup": "blue",
+ }
+
+ return [__(doc.status), color_map[doc.status], true];
+ }
};
diff --git a/erpnext/manufacturing/doctype/workstation_working_hour/workstation_working_hour.json b/erpnext/manufacturing/doctype/workstation_working_hour/workstation_working_hour.json
index a79182fb31b..b185f7d29de 100644
--- a/erpnext/manufacturing/doctype/workstation_working_hour/workstation_working_hour.json
+++ b/erpnext/manufacturing/doctype/workstation_working_hour/workstation_working_hour.json
@@ -1,150 +1,58 @@
{
- "allow_copy": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2014-12-24 14:46:40.678236",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "creation": "2014-12-24 14:46:40.678236",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "start_time",
+ "hours",
+ "column_break_2",
+ "end_time",
+ "enabled"
+ ],
"fields": [
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "start_time",
- "fieldtype": "Time",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Start Time",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "start_time",
+ "fieldtype": "Time",
+ "in_list_view": 1,
+ "label": "Start Time",
+ "reqd": 1
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_2",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "end_time",
- "fieldtype": "Time",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "End Time",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "end_time",
+ "fieldtype": "Time",
+ "in_list_view": 1,
+ "label": "End Time",
+ "reqd": 1
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "1",
- "fieldname": "enabled",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Enabled",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "default": "1",
+ "fieldname": "enabled",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Enabled"
+ },
+ {
+ "fieldname": "hours",
+ "fieldtype": "Float",
+ "label": "Hours",
+ "read_only": 1
}
- ],
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
-
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2016-12-13 05:02:36.754145",
- "modified_by": "Administrator",
- "module": "Manufacturing",
- "name": "Workstation Working Hour",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_seen": 0
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2023-10-25 14:48:29.697498",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Workstation Working Hour",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/page/visual_plant_floor/__init__.py b/erpnext/manufacturing/page/visual_plant_floor/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/manufacturing/page/visual_plant_floor/visual_plant_floor.js b/erpnext/manufacturing/page/visual_plant_floor/visual_plant_floor.js
new file mode 100644
index 00000000000..38667e8d795
--- /dev/null
+++ b/erpnext/manufacturing/page/visual_plant_floor/visual_plant_floor.js
@@ -0,0 +1,13 @@
+
+
+frappe.pages['visual-plant-floor'].on_page_load = function(wrapper) {
+ var page = frappe.ui.make_app_page({
+ parent: wrapper,
+ title: 'Visual Plant Floor',
+ single_column: true
+ });
+
+ frappe.visual_plant_floor = new frappe.ui.VisualPlantFloor(
+ {wrapper: $(wrapper).find('.layout-main-section')}, wrapper.page
+ );
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/page/visual_plant_floor/visual_plant_floor.json b/erpnext/manufacturing/page/visual_plant_floor/visual_plant_floor.json
new file mode 100644
index 00000000000..a907e973e34
--- /dev/null
+++ b/erpnext/manufacturing/page/visual_plant_floor/visual_plant_floor.json
@@ -0,0 +1,29 @@
+{
+ "content": null,
+ "creation": "2023-10-06 15:17:39.215300",
+ "docstatus": 0,
+ "doctype": "Page",
+ "idx": 0,
+ "modified": "2023-10-06 15:18:00.622073",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "visual-plant-floor",
+ "owner": "Administrator",
+ "page_name": "visual-plant-floor",
+ "roles": [
+ {
+ "role": "Manufacturing User"
+ },
+ {
+ "role": "Manufacturing Manager"
+ },
+ {
+ "role": "Operator"
+ }
+ ],
+ "script": null,
+ "standard": "Yes",
+ "style": null,
+ "system_page": 0,
+ "title": "Visual Plant Floor"
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
index 8e0785074fa..d2520d6b7eb 100644
--- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
+++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
@@ -1,6 +1,6 @@
{
"charts": [],
- "content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"YHCQG3wAGv\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Creator\",\"col\":3}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
+ "content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"YHCQG3wAGv\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Creator\",\"col\":3}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"Ubj6zXcmIQ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Plant Floor\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
"creation": "2020-03-02 17:11:37.032604",
"custom_blocks": [],
"docstatus": 0,
@@ -316,7 +316,7 @@
"type": "Link"
}
],
- "modified": "2023-08-08 22:28:39.633891",
+ "modified": "2024-01-30 21:49:58.577218",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing",
@@ -336,6 +336,13 @@
"type": "URL",
"url": "https://frappe.school/courses/manufacturing?utm_source=in_app"
},
+ {
+ "color": "Grey",
+ "doc_view": "List",
+ "label": "Plant Floor",
+ "link_to": "Plant Floor",
+ "type": "DocType"
+ },
{
"color": "Grey",
"doc_view": "List",
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index a3d4d880b29..0241afcf030 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -7,6 +7,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
super.setup();
let me = this;
+ this.set_fields_onload_for_line_item();
this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle'];
frappe.flags.hide_serial_batch_dialog = true;
@@ -105,6 +106,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
frappe.ui.form.on(this.frm.doctype + " Item", {
items_add: function(frm, cdt, cdn) {
+ debugger
var item = frappe.get_doc(cdt, cdn);
if (!item.warehouse && frm.doc.set_warehouse) {
item.warehouse = frm.doc.set_warehouse;
@@ -118,6 +120,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
item.from_warehouse = frm.doc.set_from_warehouse;
}
+ if (item.docstatus === 0
+ && frappe.meta.has_field(item.doctype, "use_serial_batch_fields")
+ && cint(frappe.user_defaults?.use_serial_batch_fields) === 1
+ ) {
+ frappe.model.set_value(item.doctype, item.name, "use_serial_batch_fields", 1);
+ }
+
erpnext.accounts.dimensions.copy_dimension_from_first_row(frm, cdt, cdn, 'items');
}
});
@@ -222,7 +231,19 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
};
});
}
+ }
+ set_fields_onload_for_line_item() {
+ if (this.frm.is_new && this.frm.doc?.items) {
+ this.frm.doc.items.forEach(item => {
+ if (item.docstatus === 0
+ && frappe.meta.has_field(item.doctype, "use_serial_batch_fields")
+ && cint(frappe.user_defaults?.use_serial_batch_fields) === 1
+ ) {
+ frappe.model.set_value(item.doctype, item.name, "use_serial_batch_fields", 1);
+ }
+ })
+ }
}
toggle_enable_for_stock_uom(field) {
@@ -462,6 +483,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
this.frm.doc.doctype === 'Delivery Note') {
show_batch_dialog = 1;
}
+
+ if (show_batch_dialog && item.use_serial_batch_fields === 1) {
+ show_batch_dialog = 0;
+ }
+
item.barcode = null;
@@ -502,6 +528,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
project: item.project || me.frm.doc.project,
qty: item.qty || 1,
net_rate: item.rate,
+ base_net_rate: item.base_net_rate,
stock_qty: item.stock_qty,
conversion_factor: item.conversion_factor,
weight_per_unit: item.weight_per_unit,
@@ -705,10 +732,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
item.serial_no = item.serial_no.replace(/,/g, '\n');
item.conversion_factor = item.conversion_factor || 1;
refresh_field("serial_no", item.name, item.parentfield);
- if (!doc.is_return && cint(frappe.user_defaults.set_qty_in_transactions_based_on_serial_no_input)) {
+ if (!doc.is_return) {
setTimeout(() => {
me.update_qty(cdt, cdn);
- }, 10000);
+ }, 3000);
}
}
}
@@ -798,14 +825,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
let selling_doctypes_for_tc = ["Sales Invoice", "Quotation", "Sales Order", "Delivery Note"];
if (company_doc.default_selling_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") &&
- selling_doctypes_for_tc.indexOf(me.frm.doc.doctype) != -1) {
+ selling_doctypes_for_tc.includes(me.frm.doc.doctype) && !me.frm.doc.tc_name) {
me.frm.set_value("tc_name", company_doc.default_selling_terms);
}
let buying_doctypes_for_tc = ["Request for Quotation", "Supplier Quotation", "Purchase Order",
"Material Request", "Purchase Receipt"];
// Purchase Invoice is excluded as per issue #3345
if (company_doc.default_buying_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") &&
- buying_doctypes_for_tc.indexOf(me.frm.doc.doctype) != -1) {
+ buying_doctypes_for_tc.includes(me.frm.doc.doctype) && !me.frm.doc.tc_name) {
me.frm.set_value("tc_name", company_doc.default_buying_terms);
}
}
@@ -1200,8 +1227,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
let item = frappe.get_doc(cdt, cdn);
// item.pricing_rules = ''
frappe.run_serially([
- () => this.remove_pricing_rule(item),
+ () => this.remove_pricing_rule_for_item(item),
() => this.conversion_factor(doc, cdt, cdn, true),
+ () => this.apply_price_list(item, true), //reapply price list before applying pricing rule
() => this.calculate_stock_uom_rate(doc, cdt, cdn),
() => this.apply_pricing_rule(item, true)
]);
@@ -1240,20 +1268,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
}
- sync_bundle_data() {
- let doctypes = ["Sales Invoice", "Purchase Invoice", "Delivery Note", "Purchase Receipt"];
-
- if (this.frm.is_new() && doctypes.includes(this.frm.doc.doctype)) {
- const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm});
- barcode_scanner.sync_bundle_data();
- barcode_scanner.remove_item_from_localstorage();
- }
- }
-
- before_save(doc) {
- this.sync_bundle_data();
- }
-
service_start_date(frm, cdt, cdn) {
var child = locals[cdt][cdn];
@@ -1448,8 +1462,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
ignore_pricing_rule() {
if(this.frm.doc.ignore_pricing_rule) {
- var me = this;
- var item_list = [];
+ let me = this;
+ let item_list = [];
$.each(this.frm.doc["items"] || [], function(i, d) {
if (d.item_code) {
@@ -1488,6 +1502,34 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
}
+ remove_pricing_rule_for_item(item) {
+ let me = this;
+ return this.frm.call({
+ method: "erpnext.accounts.doctype.pricing_rule.pricing_rule.remove_pricing_rule_for_item",
+ args: {
+ pricing_rules: item.pricing_rules,
+ item_details: {
+ "doctype": item.doctype,
+ "name": item.name,
+ "item_code": item.item_code,
+ "pricing_rules": item.pricing_rules,
+ "parenttype": item.parenttype,
+ "parent": item.parent,
+ "price_list_rate": item.price_list_rate
+ },
+ item_code: item.item_code,
+ rate: item.price_list_rate,
+ },
+ callback: function(r) {
+ if (!r.exc && r.message) {
+ me.remove_pricing_rule(r.message);
+ me.calculate_taxes_and_totals();
+ if(me.frm.doc.apply_discount_on) me.frm.trigger("apply_discount_on");
+ }
+ }
+ });
+ }
+
apply_pricing_rule(item, calculate_taxes_and_totals) {
var me = this;
var args = this._get_args(item);
@@ -1712,8 +1754,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
this.frm.set_value("plc_conversion_rate", "");
}
- var me = this;
- var args = this._get_args(item);
+ let me = this;
+ let args = this._get_args(item);
if (!((args.items && args.items.length) || args.price_list)) {
return;
}
@@ -1755,7 +1797,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
"discount_amount", "margin_rate_or_amount", "rate_with_margin"];
if(item.remove_free_item) {
- var items = [];
+ let items = [];
me.frm.doc.items.forEach(d => {
if(d.item_code != item.remove_free_item || !d.is_free_item) {
@@ -1873,7 +1915,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if (item.item_code) {
// Use combination of name and item code in case same item is added multiple times
item_codes.push([item.item_code, item.name]);
- item_rates[item.name] = item.net_rate;
+ item_rates[item.name] = item.base_net_rate;
item_tax_templates[item.name] = item.item_tax_template;
}
});
diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js
index dee9a06f052..b847e5729f5 100644
--- a/erpnext/public/js/erpnext.bundle.js
+++ b/erpnext/public/js/erpnext.bundle.js
@@ -5,6 +5,8 @@ import "./sms_manager";
import "./utils/party";
import "./controllers/stock_controller";
import "./payment/payments";
+import "./templates/visual_plant_floor_template.html";
+import "./plant_floor_visual/visual_plant";
import "./controllers/taxes_and_totals";
import "./controllers/transaction";
import "./templates/item_selector.html";
diff --git a/erpnext/public/js/plant_floor_visual/visual_plant.js b/erpnext/public/js/plant_floor_visual/visual_plant.js
new file mode 100644
index 00000000000..8cd73adc574
--- /dev/null
+++ b/erpnext/public/js/plant_floor_visual/visual_plant.js
@@ -0,0 +1,157 @@
+class VisualPlantFloor {
+ constructor({wrapper, skip_filters=false, plant_floor=null}, page=null) {
+ this.wrapper = wrapper;
+ this.plant_floor = plant_floor;
+ this.skip_filters = skip_filters;
+
+ this.make();
+ if (!this.skip_filters) {
+ this.page = page;
+ this.add_filter();
+ this.prepare_menu();
+ }
+ }
+
+ make() {
+ this.wrapper.append(`
+
+ `);
+
+ if (!this.skip_filters) {
+ this.filter_wrapper = this.wrapper.find('.plant-floor-filter');
+ this.visualization_wrapper = this.wrapper.find('.plant-floor-visualization');
+ } else if(this.plant_floor) {
+ this.wrapper.find('.plant-floor').css('border', 'none');
+ this.prepare_data();
+ }
+ }
+
+ prepare_data() {
+ frappe.call({
+ method: 'erpnext.manufacturing.doctype.workstation.workstation.get_workstations',
+ args: {
+ plant_floor: this.plant_floor,
+ },
+ callback: (r) => {
+ this.workstations = r.message;
+ this.render_workstations();
+ }
+ });
+ }
+
+ add_filter() {
+ this.plant_floor = frappe.ui.form.make_control({
+ df: {
+ fieldtype: 'Link',
+ options: 'Plant Floor',
+ fieldname: 'plant_floor',
+ label: __('Plant Floor'),
+ reqd: 1,
+ onchange: () => {
+ this.render_plant_visualization();
+ }
+ },
+ parent: this.filter_wrapper,
+ render_input: true,
+ });
+
+ this.plant_floor.$wrapper.addClass('form-column col-sm-2');
+
+ this.workstation_type = frappe.ui.form.make_control({
+ df: {
+ fieldtype: 'Link',
+ options: 'Workstation Type',
+ fieldname: 'workstation_type',
+ label: __('Machine Type'),
+ onchange: () => {
+ this.render_plant_visualization();
+ }
+ },
+ parent: this.filter_wrapper,
+ render_input: true,
+ });
+
+ this.workstation_type.$wrapper.addClass('form-column col-sm-2');
+
+ this.workstation = frappe.ui.form.make_control({
+ df: {
+ fieldtype: 'Link',
+ options: 'Workstation',
+ fieldname: 'workstation',
+ label: __('Machine'),
+ onchange: () => {
+ this.render_plant_visualization();
+ },
+ get_query: () => {
+ if (this.workstation_type.get_value()) {
+ return {
+ filters: {
+ 'workstation_type': this.workstation_type.get_value() || ''
+ }
+ }
+ }
+ }
+ },
+ parent: this.filter_wrapper,
+ render_input: true,
+ });
+
+ this.workstation.$wrapper.addClass('form-column col-sm-2');
+
+ this.workstation_status = frappe.ui.form.make_control({
+ df: {
+ fieldtype: 'Select',
+ options: '\nProduction\nOff\nIdle\nProblem\nMaintenance\nSetup',
+ fieldname: 'workstation_status',
+ label: __('Status'),
+ onchange: () => {
+ this.render_plant_visualization();
+ },
+ },
+ parent: this.filter_wrapper,
+ render_input: true,
+ });
+ }
+
+ render_plant_visualization() {
+ let plant_floor = this.plant_floor.get_value();
+
+ if (plant_floor) {
+ frappe.call({
+ method: 'erpnext.manufacturing.doctype.workstation.workstation.get_workstations',
+ args: {
+ plant_floor: plant_floor,
+ workstation_type: this.workstation_type.get_value(),
+ workstation: this.workstation.get_value(),
+ workstation_status: this.workstation_status.get_value()
+ },
+ callback: (r) => {
+ this.workstations = r.message;
+ this.render_workstations();
+ }
+ });
+ }
+ }
+
+ render_workstations() {
+ this.wrapper.find('.plant-floor-container').empty();
+ let template = frappe.render_template("visual_plant_floor_template", {
+ workstations: this.workstations
+ });
+
+ $(template).appendTo(this.wrapper.find('.plant-floor-container'));
+ }
+
+ prepare_menu() {
+ this.page.add_menu_item(__('Refresh'), () => {
+ this.render_plant_visualization();
+ });
+ }
+}
+
+frappe.ui.VisualPlantFloor = VisualPlantFloor;
\ No newline at end of file
diff --git a/erpnext/public/js/templates/visual_plant_floor_template.html b/erpnext/public/js/templates/visual_plant_floor_template.html
new file mode 100644
index 00000000000..2e67085c022
--- /dev/null
+++ b/erpnext/public/js/templates/visual_plant_floor_template.html
@@ -0,0 +1,19 @@
+{% $.each(workstations, (idx, row) => { %}
+
+
+
+
{{row.status}}
+
{{row.workstation_name}}
+
+
+{% }); %}
\ No newline at end of file
diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js
index aacab0fe6c1..4d1c0c1ad3d 100644
--- a/erpnext/public/js/utils/barcode_scanner.js
+++ b/erpnext/public/js/utils/barcode_scanner.js
@@ -1,12 +1,15 @@
erpnext.utils.BarcodeScanner = class BarcodeScanner {
constructor(opts) {
this.frm = opts.frm;
+ // frappe.flags.trigger_from_barcode_scanner is used for custom scripts
// field from which to capture input of scanned data
this.scan_field_name = opts.scan_field_name || "scan_barcode";
this.scan_barcode_field = this.frm.fields_dict[this.scan_field_name];
this.barcode_field = opts.barcode_field || "barcode";
+ this.serial_no_field = opts.serial_no_field || "serial_no";
+ this.batch_no_field = opts.batch_no_field || "batch_no";
this.uom_field = opts.uom_field || "uom";
this.qty_field = opts.qty_field || "qty";
// field name on row which defines max quantity to be scanned e.g. picklist
@@ -105,53 +108,52 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
this.frm.has_items = false;
}
- if (serial_no) {
- this.is_duplicate_serial_no(row, item_code, serial_no)
- .then((is_duplicate) => {
- if (!is_duplicate) {
- this.run_serially_tasks(row, data, resolve);
- } else {
- this.clean_up();
- reject();
- return;
- }
- });
- } else {
- this.run_serially_tasks(row, data, resolve);
+ if (this.is_duplicate_serial_no(row, serial_no)) {
+ this.clean_up();
+ reject();
+ return;
}
-
+ frappe.run_serially([
+ () => this.set_selector_trigger_flag(data),
+ () => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => {
+ this.show_scan_message(row.idx, row.item_code, qty);
+ }),
+ () => this.set_barcode_uom(row, uom),
+ () => this.set_serial_no(row, serial_no),
+ () => this.set_batch_no(row, batch_no),
+ () => this.set_barcode(row, barcode),
+ () => this.clean_up(),
+ () => this.revert_selector_flag(),
+ () => resolve(row)
+ ]);
});
}
- run_serially_tasks(row, data, resolve) {
- const {item_code, barcode, batch_no, serial_no, uom} = data;
+ // batch and serial selector is reduandant when all info can be added by scan
+ // this flag on item row is used by transaction.js to avoid triggering selector
+ set_selector_trigger_flag(data) {
+ const {batch_no, serial_no, has_batch_no, has_serial_no} = data;
- frappe.run_serially([
- () => this.set_serial_and_batch(row, item_code, serial_no, batch_no),
- () => this.set_barcode(row, barcode),
- () => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => {
- this.show_scan_message(row.idx, row.item_code, qty);
- }),
- () => this.set_barcode_uom(row, uom),
- () => this.clean_up(),
- () => {
- if (row.serial_and_batch_bundle && !this.frm.is_new()) {
- this.frm.save();
- }
+ const require_selecting_batch = has_batch_no && !batch_no;
+ const require_selecting_serial = has_serial_no && !serial_no;
- frappe.flags.trigger_from_barcode_scanner = false;
- },
- () => resolve(row),
- ]);
+ if (!(require_selecting_batch || require_selecting_serial)) {
+ frappe.flags.hide_serial_batch_dialog = true;
+ }
+ }
+
+ revert_selector_flag() {
+ frappe.flags.hide_serial_batch_dialog = false;
+ frappe.flags.trigger_from_barcode_scanner = false;
}
set_item(row, item_code, barcode, batch_no, serial_no) {
return new Promise(resolve => {
const increment = async (value = 1) => {
- const item_data = {item_code: item_code};
- item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value);
+ const item_data = {item_code: item_code, use_serial_batch_fields: 1.0};
frappe.flags.trigger_from_barcode_scanner = true;
+ item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value);
await frappe.model.set_value(row.doctype, row.name, item_data);
return value;
};
@@ -160,6 +162,8 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
frappe.prompt(__("Please enter quantity for item {0}", [item_code]), ({value}) => {
increment(value).then((value) => resolve(value));
});
+ } else if (this.frm.has_items) {
+ this.prepare_item_for_scan(row, item_code, barcode, batch_no, serial_no);
} else {
increment().then((value) => resolve(value));
}
@@ -182,8 +186,9 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
frappe.model.set_value(row.doctype, row.name, item_data);
frappe.run_serially([
+ () => this.set_batch_no(row, this.dialog.get_value("batch_no")),
() => this.set_barcode(row, this.dialog.get_value("barcode")),
- () => this.set_serial_and_batch(row, item_code, this.dialog.get_value("serial_no"), this.dialog.get_value("batch_no")),
+ () => this.set_serial_no(row, this.dialog.get_value("serial_no")),
() => this.add_child_for_remaining_qty(row),
() => this.clean_up()
]);
@@ -337,144 +342,32 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
}
}
- async set_serial_and_batch(row, item_code, serial_no, batch_no) {
- if (this.frm.is_new() || !row.serial_and_batch_bundle) {
- this.set_bundle_in_localstorage(row, item_code, serial_no, batch_no);
- } else if(row.serial_and_batch_bundle) {
- frappe.call({
- method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.update_serial_or_batch",
- args: {
- bundle_id: row.serial_and_batch_bundle,
- serial_no: serial_no,
- batch_no: batch_no,
- },
- })
- }
- }
+ async set_serial_no(row, serial_no) {
+ if (serial_no && frappe.meta.has_field(row.doctype, this.serial_no_field)) {
+ const existing_serial_nos = row[this.serial_no_field];
+ let new_serial_nos = "";
- get_key_for_localstorage() {
- let parts = this.frm.doc.name.split("-");
- return parts[parts.length - 1] + this.frm.doc.doctype;
- }
-
- update_localstorage_scanned_data() {
- let docname = this.frm.doc.name
- if (localStorage[docname]) {
- let items = JSON.parse(localStorage[docname]);
- let existing_items = this.frm.doc.items.map(d => d.item_code);
- if (!existing_items.length) {
- localStorage.removeItem(docname);
- return;
+ if (!!existing_serial_nos) {
+ new_serial_nos = existing_serial_nos + "\n" + serial_no;
+ } else {
+ new_serial_nos = serial_no;
}
-
- for (let item_code in items) {
- if (!existing_items.includes(item_code)) {
- delete items[item_code];
- }
- }
-
- localStorage[docname] = JSON.stringify(items);
+ await frappe.model.set_value(row.doctype, row.name, this.serial_no_field, new_serial_nos);
}
}
- async set_bundle_in_localstorage(row, item_code, serial_no, batch_no) {
- let docname = this.frm.doc.name
-
- let entries = JSON.parse(localStorage.getItem(docname));
- if (!entries) {
- entries = {};
- }
-
- let key = item_code;
- if (!entries[key]) {
- entries[key] = [];
- }
-
- let existing_row = [];
- if (!serial_no && batch_no) {
- existing_row = entries[key].filter((e) => e.batch_no === batch_no);
- if (existing_row.length) {
- existing_row[0].qty += 1;
- }
- } else if (serial_no) {
- existing_row = entries[key].filter((e) => e.serial_no === serial_no);
- if (existing_row.length) {
- frappe.throw(__("Serial No {0} has already scanned.", [serial_no]));
- }
- }
-
- if (!existing_row.length) {
- entries[key].push({
- "serial_no": serial_no,
- "batch_no": batch_no,
- "qty": 1
- });
- }
-
- localStorage.setItem(docname, JSON.stringify(entries));
-
- // Auto remove from localstorage after 1 hour
- setTimeout(() => {
- localStorage.removeItem(docname);
- }, 3600000)
- }
-
- remove_item_from_localstorage() {
- let docname = this.frm.doc.name;
- if (localStorage[docname]) {
- localStorage.removeItem(docname);
- }
- }
-
- async sync_bundle_data() {
- let docname = this.frm.doc.name;
-
- if (localStorage[docname]) {
- let entries = JSON.parse(localStorage[docname]);
- if (entries) {
- for (let entry in entries) {
- let row = this.frm.doc.items.filter((item) => {
- if (item.item_code === entry) {
- return true;
- }
- })[0];
-
- if (row) {
- this.create_serial_and_batch_bundle(row, entries, entry)
- .then(() => {
- if (!entries) {
- localStorage.removeItem(docname);
- }
- });
- }
- }
- }
- }
- }
-
- async create_serial_and_batch_bundle(row, entries, key) {
- frappe.call({
- method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.add_serial_batch_ledgers",
- args: {
- entries: entries[key],
- child_row: row,
- doc: this.frm.doc,
- warehouse: row.warehouse,
- do_not_save: 1
- },
- callback: function(r) {
- row.serial_and_batch_bundle = r.message.name;
- delete entries[key];
- }
- })
- }
-
async set_barcode_uom(row, uom) {
if (uom && frappe.meta.has_field(row.doctype, this.uom_field)) {
await frappe.model.set_value(row.doctype, row.name, this.uom_field, uom);
}
}
+ async set_batch_no(row, batch_no) {
+ if (batch_no && frappe.meta.has_field(row.doctype, this.batch_no_field)) {
+ await frappe.model.set_value(row.doctype, row.name, this.batch_no_field, batch_no);
+ }
+ }
+
async set_barcode(row, barcode) {
if (barcode && frappe.meta.has_field(row.doctype, this.barcode_field)) {
await frappe.model.set_value(row.doctype, row.name, this.barcode_field, barcode);
@@ -490,58 +383,13 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
}
}
- async is_duplicate_serial_no(row, item_code, serial_no) {
- let is_duplicate = false;
- const promise = new Promise((resolve, reject) => {
- if (this.frm.is_new() || !row.serial_and_batch_bundle) {
- is_duplicate = this.check_duplicate_serial_no_in_localstorage(item_code, serial_no);
- if (is_duplicate) {
- this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
- }
+ is_duplicate_serial_no(row, serial_no) {
+ const is_duplicate = row[this.serial_no_field]?.includes(serial_no);
- resolve(is_duplicate);
- } else if (row.serial_and_batch_bundle) {
- this.check_duplicate_serial_no_in_db(row, serial_no, (r) => {
- if (r.message) {
- this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
- }
-
- is_duplicate = r.message;
- resolve(is_duplicate);
- })
- }
- });
-
- return await promise;
- }
-
- check_duplicate_serial_no_in_db(row, serial_no, response) {
- frappe.call({
- method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.is_duplicate_serial_no",
- args: {
- serial_no: serial_no,
- bundle_id: row.serial_and_batch_bundle
- },
- callback(r) {
- response(r);
- }
- });
- }
-
- check_duplicate_serial_no_in_localstorage(item_code, serial_no) {
- let docname = this.frm.doc.name
- let entries = JSON.parse(localStorage.getItem(docname));
-
- if (!entries) {
- return false;
+ if (is_duplicate) {
+ this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
}
-
- let existing_row = [];
- if (entries[item_code]) {
- existing_row = entries[item_code].filter((e) => e.serial_no === serial_no);
- }
-
- return existing_row.length;
+ return is_duplicate;
}
get_row_to_modify_on_scan(item_code, batch_no, uom, barcode) {
@@ -587,4 +435,4 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
show_alert(msg, indicator, duration=3) {
frappe.show_alert({message: msg, indicator: indicator}, duration);
}
-};
+};
\ No newline at end of file
diff --git a/erpnext/public/scss/erpnext.scss b/erpnext/public/scss/erpnext.scss
index 8ab5973debd..1626b7c894d 100644
--- a/erpnext/public/scss/erpnext.scss
+++ b/erpnext/public/scss/erpnext.scss
@@ -490,3 +490,53 @@ body[data-route="pos"] {
.exercise-col {
padding: 10px;
}
+
+.plant-floor, .workstation-wrapper, .workstation-card p {
+ border-radius: var(--border-radius-md);
+ border: 1px solid var(--border-color);
+ box-shadow: none;
+ background-color: var(--card-bg);
+ position: relative;
+}
+
+.plant-floor {
+ padding-bottom: 25px;
+}
+
+.plant-floor-filter {
+ padding-top: 10px;
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.plant-floor-container {
+ display: grid;
+ grid-template-columns: repeat(6,minmax(0,1fr));
+ gap: var(--margin-xl);
+}
+
+@media screen and (max-width: 620px) {
+ .plant-floor-container {
+ grid-template-columns: repeat(2,minmax(0,1fr));
+ }
+}
+
+.plant-floor-container .workstation-card {
+ padding: 5px;
+}
+
+.plant-floor-container .workstation-image-link {
+ width: 100%;
+ font-size: 50px;
+ margin: var(--margin-sm);
+ min-height: 9rem;
+}
+
+.workstation-abbr {
+ display: flex;
+ background-color: var(--control-bg);
+ height:100%;
+ width:100%;
+ align-items: center;
+ justify-content: center;
+}
\ No newline at end of file
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index 835b545cbed..75341feeba5 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -230,6 +230,7 @@ class Customer(TransactionBase):
if self.flags.is_new_doc:
self.link_lead_address_and_contact()
+ self.copy_communication()
self.update_customer_groups()
@@ -287,6 +288,17 @@ class Customer(TransactionBase):
linked_doc.append("links", dict(link_doctype="Customer", link_name=self.name))
linked_doc.save(ignore_permissions=self.flags.ignore_permissions)
+ def copy_communication(self):
+ if not self.lead_name or not frappe.db.get_single_value(
+ "CRM Settings", "carry_forward_communication_and_comments"
+ ):
+ return
+
+ from erpnext.crm.utils import copy_comments, link_communications
+
+ copy_comments("Lead", self.lead_name, self)
+ link_communications("Lead", self.lead_name, self)
+
def validate_name_with_customer_group(self):
if frappe.db.exists("Customer Group", self.name):
frappe.throw(
diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.json b/erpnext/selling/doctype/product_bundle/product_bundle.json
index c4f21b61b9e..1c37b854b9a 100644
--- a/erpnext/selling/doctype/product_bundle/product_bundle.json
+++ b/erpnext/selling/doctype/product_bundle/product_bundle.json
@@ -2,7 +2,7 @@
"actions": [],
"allow_import": 1,
"creation": "2013-06-20 11:53:21",
- "description": "Aggregate group of **Items** into another **Item**. This is useful if you are bundling a certain **Items** into a package and you maintain stock of the packed **Items** and not the aggregate **Item**. \n\nThe package **Item** will have \"Is Stock Item\" as \"No\" and \"Is Sales Item\" as \"Yes\".\n\nFor Example: If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.\n\nNote: BOM = Bill of Materials",
+ "description": "Aggregate a group of Items into another Item. This is useful if you are maintaining the stock of the packed items and not the bundled item",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
@@ -77,7 +77,7 @@
"icon": "fa fa-sitemap",
"idx": 1,
"links": [],
- "modified": "2023-11-22 15:20:46.805114",
+ "modified": "2024-01-30 13:57:04.951788",
"modified_by": "Administrator",
"module": "Selling",
"name": "Product Bundle",
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 5ef2c50146a..f00e6ac5122 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -904,6 +904,7 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None):
target.run_method("set_missing_values")
target.run_method("set_po_nos")
target.run_method("calculate_taxes_and_totals")
+ target.run_method("set_use_serial_batch_fields")
if source.company_address:
target.update({"company_address": source.company_address})
@@ -1024,6 +1025,7 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
target.run_method("set_missing_values")
target.run_method("set_po_nos")
target.run_method("calculate_taxes_and_totals")
+ target.run_method("set_use_serial_batch_fields")
if source.company_address:
target.update({"company_address": source.company_address})
@@ -1606,7 +1608,11 @@ def create_pick_list(source_name, target_doc=None):
"Sales Order",
source_name,
{
- "Sales Order": {"doctype": "Pick List", "validation": {"docstatus": ["=", 1]}},
+ "Sales Order": {
+ "doctype": "Pick List",
+ "field_map": {"set_warehouse": "parent_warehouse"},
+ "validation": {"docstatus": ["=", 1]},
+ },
"Sales Order Item": {
"doctype": "Pick List Item",
"field_map": {"parent": "sales_order", "name": "sales_order_item"},
diff --git a/erpnext/setup/doctype/item_group/item_group.json b/erpnext/setup/doctype/item_group/item_group.json
index dfa5a8ed0a7..7c9233fa1f3 100644
--- a/erpnext/setup/doctype/item_group/item_group.json
+++ b/erpnext/setup/doctype/item_group/item_group.json
@@ -4,7 +4,7 @@
"allow_rename": 1,
"autoname": "field:item_group_name",
"creation": "2013-03-28 10:35:29",
- "description": "Item Classification",
+ "description": "An Item Group is a way to classify items based on types.",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
@@ -135,7 +135,7 @@
"is_tree": 1,
"links": [],
"max_attachments": 3,
- "modified": "2023-10-12 13:44:13.611287",
+ "modified": "2024-01-30 14:08:38.485616",
"modified_by": "Administrator",
"module": "Setup",
"name": "Item Group",
diff --git a/erpnext/setup/doctype/sales_person/sales_person.json b/erpnext/setup/doctype/sales_person/sales_person.json
index e526ac42ba8..79bd8411ee2 100644
--- a/erpnext/setup/doctype/sales_person/sales_person.json
+++ b/erpnext/setup/doctype/sales_person/sales_person.json
@@ -4,7 +4,7 @@
"allow_rename": 1,
"autoname": "field:sales_person_name",
"creation": "2013-01-10 16:34:24",
- "description": "All Sales Transactions can be tagged against multiple **Sales Persons** so that you can set and monitor targets.",
+ "description": "All Sales Transactions can be tagged against multiple Sales Persons so that you can set and monitor targets.",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
@@ -145,10 +145,11 @@
"idx": 1,
"is_tree": 1,
"links": [],
- "modified": "2020-03-18 18:11:13.968024",
+ "modified": "2024-01-30 13:57:26.436991",
"modified_by": "Administrator",
"module": "Setup",
"name": "Sales Person",
+ "naming_rule": "By fieldname",
"nsm_parent_field": "parent_sales_person",
"owner": "Administrator",
"permissions": [
@@ -181,5 +182,6 @@
"search_fields": "parent_sales_person",
"show_name_in_global_search": 1,
"sort_field": "modified",
- "sort_order": "ASC"
+ "sort_order": "ASC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json
index f884864acfa..76e52aefeba 100644
--- a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json
+++ b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json
@@ -4,7 +4,7 @@
"allow_rename": 1,
"autoname": "field:title",
"creation": "2013-01-10 16:34:24",
- "description": "Standard Terms and Conditions that can be added to Sales and Purchases.\n\nExamples:\n\n1. Validity of the offer.\n1. Payment Terms (In Advance, On Credit, part advance etc).\n1. What is extra (or payable by the Customer).\n1. Safety / usage warning.\n1. Warranty if any.\n1. Returns Policy.\n1. Terms of shipping, if applicable.\n1. Ways of addressing disputes, indemnity, liability, etc.\n1. Address and Contact of your Company.",
+ "description": "Standard Terms and Conditions that can be added to Sales and Purchases. Examples: Validity of the offer, Payment Terms, Safety and Usage, etc.",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
@@ -77,7 +77,7 @@
"icon": "icon-legal",
"idx": 1,
"links": [],
- "modified": "2023-02-01 14:33:39.246532",
+ "modified": "2024-01-30 12:47:52.325531",
"modified_by": "Administrator",
"module": "Setup",
"name": "Terms and Conditions",
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 7d7b0cd4769..df45fdded89 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -398,6 +398,8 @@ class DeliveryNote(SellingController):
self.check_credit_limit()
elif self.issue_credit_note:
self.make_return_invoice()
+
+ self.make_bundle_using_old_serial_batch_fields()
# Updating stock ledger should always be called after updating prevdoc status,
# because updating reserved qty in bin depends upon updated delivered qty in SO
self.update_stock_ledger()
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index dae42895edb..7889f95c605 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -189,7 +189,6 @@ class TestDeliveryNote(FrappeTestCase):
},
)
- frappe.flags.ignore_serial_batch_bundle_validation = True
serial_nos = [
"OSN-1",
"OSN-2",
@@ -228,6 +227,8 @@ class TestDeliveryNote(FrappeTestCase):
)
se_doc.items[0].serial_no = "\n".join(serial_nos)
+
+ frappe.flags.use_serial_and_batch_fields = True
se_doc.submit()
self.assertEqual(sorted(get_serial_nos(se_doc.items[0].serial_no)), sorted(serial_nos))
@@ -283,6 +284,8 @@ class TestDeliveryNote(FrappeTestCase):
self.assertTrue(serial_no in serial_nos)
self.assertFalse(serial_no in returned_serial_nos1)
+ frappe.flags.use_serial_and_batch_fields = False
+
def test_sales_return_for_non_bundled_items_partial(self):
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
@@ -1552,7 +1555,7 @@ def create_delivery_note(**args):
dn.return_against = args.return_against
bundle_id = None
- if args.get("batch_no") or args.get("serial_no"):
+ if not args.use_serial_batch_fields and (args.get("batch_no") or args.get("serial_no")):
type_of_transaction = args.type_of_transaction or "Outward"
if dn.is_return:
@@ -1594,6 +1597,9 @@ def create_delivery_note(**args):
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"target_warehouse": args.target_warehouse,
+ "use_serial_batch_fields": args.use_serial_batch_fields,
+ "serial_no": args.serial_no if args.use_serial_batch_fields else None,
+ "batch_no": args.batch_no if args.use_serial_batch_fields else None,
},
)
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 a44b9ac44be..247672fe126 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -80,8 +80,11 @@
"section_break_40",
"pick_serial_and_batch",
"serial_and_batch_bundle",
+ "use_serial_batch_fields",
"column_break_eaoe",
+ "section_break_qyjv",
"serial_no",
+ "column_break_rxvc",
"batch_no",
"available_qty_section",
"actual_batch_qty",
@@ -850,6 +853,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
@@ -859,6 +863,7 @@
"search_index": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "pick_serial_and_batch",
"fieldtype": "Button",
"label": "Pick Serial / Batch No"
@@ -874,27 +879,40 @@
"fieldtype": "Column Break"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_no",
"fieldtype": "Text",
- "hidden": 1,
- "label": "Serial No",
- "read_only": 1
+ "label": "Serial No"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
- "hidden": 1,
"label": "Batch No",
"options": "Batch",
- "read_only": 1,
"search_index": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "use_serial_batch_fields",
+ "fieldtype": "Check",
+ "label": "Use Serial No / Batch Fields"
+ },
+ {
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
+ "fieldname": "section_break_qyjv",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_rxvc",
+ "fieldtype": "Column Break"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2023-11-14 18:37:38.638144",
+ "modified": "2024-02-04 14:10:31.750340",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",
diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py
index c11c4103e59..b76f7429728 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py
@@ -82,6 +82,7 @@ class DeliveryNoteItem(Document):
target_warehouse: DF.Link | None
total_weight: DF.Float
uom: DF.Link
+ use_serial_batch_fields: DF.Check
warehouse: DF.Link | None
weight_per_unit: DF.Float
weight_uom: DF.Link | None
diff --git a/erpnext/stock/doctype/item_price/item_price.json b/erpnext/stock/doctype/item_price/item_price.json
index f4d9bb0742d..b92026dc97b 100644
--- a/erpnext/stock/doctype/item_price/item_price.json
+++ b/erpnext/stock/doctype/item_price/item_price.json
@@ -3,7 +3,7 @@
"allow_import": 1,
"autoname": "hash",
"creation": "2013-05-02 16:29:48",
- "description": "Multiple Item prices.",
+ "description": "Log the selling and buying rate of an Item",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
@@ -220,7 +220,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2022-11-15 08:26:04.041861",
+ "modified": "2024-01-30 14:02:19.304854",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Price",
diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
index c6518b45cd7..aa5b2793c58 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
@@ -149,6 +149,13 @@ class LandedCostVoucher(Document):
self.get("items")[item_count - 1].applicable_charges += diff
def validate_applicable_charges_for_item(self):
+ if self.distribute_charges_based_on == "Distribute Manually" and len(self.taxes) > 1:
+ frappe.throw(
+ _(
+ "Please keep one Applicable Charges, when 'Distribute Charges Based On' is 'Distribute Manually'. For more charges, please create another Landed Cost Voucher."
+ )
+ )
+
based_on = self.distribute_charges_based_on.lower()
if based_on != "distribute manually":
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index 7e34f66c2b9..2487497efc9 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -462,6 +462,7 @@ def make_purchase_order(source_name, target_doc=None, args=None):
postprocess,
)
+ doclist.set_onload("load_after_mapping", False)
return doclist
diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json
index 5dd8934d43f..1daf6791d40 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.json
+++ b/erpnext/stock/doctype/packed_item/packed_item.json
@@ -20,9 +20,12 @@
"uom",
"section_break_9",
"pick_serial_and_batch",
- "serial_and_batch_bundle",
- "serial_no",
+ "use_serial_batch_fields",
"column_break_11",
+ "serial_and_batch_bundle",
+ "section_break_bgys",
+ "serial_no",
+ "column_break_qlha",
"batch_no",
"actual_batch_qty",
"section_break_13",
@@ -118,10 +121,10 @@
"fieldtype": "Section Break"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_no",
"fieldtype": "Text",
- "label": "Serial No",
- "read_only": 1
+ "label": "Serial No"
},
{
"fieldname": "column_break_11",
@@ -131,8 +134,7 @@
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
- "options": "Batch",
- "read_only": 1
+ "options": "Batch"
},
{
"fieldname": "section_break_13",
@@ -259,6 +261,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
@@ -267,16 +270,32 @@
"print_hide": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "pick_serial_and_batch",
"fieldtype": "Button",
"label": "Pick Serial / Batch No"
+ },
+ {
+ "default": "0",
+ "fieldname": "use_serial_batch_fields",
+ "fieldtype": "Check",
+ "label": "Use Serial No / Batch Fields"
+ },
+ {
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
+ "fieldname": "section_break_bgys",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_qlha",
+ "fieldtype": "Column Break"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2023-04-28 13:16:38.460806",
+ "modified": "2024-02-04 16:30:44.263964",
"modified_by": "Administrator",
"module": "Stock",
"name": "Packed Item",
diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py
index ed667c2b992..c115e33e171 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.py
+++ b/erpnext/stock/doctype/packed_item/packed_item.py
@@ -47,6 +47,7 @@ class PackedItem(Document):
serial_no: DF.Text | None
target_warehouse: DF.Link | None
uom: DF.Link | None
+ use_serial_batch_fields: DF.Check
warehouse: DF.Link | None
# end: auto-generated types
diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js
index afd6ce81386..aa0e1254968 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.js
+++ b/erpnext/stock/doctype/pick_list/pick_list.js
@@ -16,7 +16,6 @@ frappe.ui.form.on('Pick List', {
frm.set_query('parent_warehouse', () => {
return {
filters: {
- 'is_group': 1,
'company': frm.doc.company
}
};
diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json
index 7259dc00a81..bd84aadef74 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.json
+++ b/erpnext/stock/doctype/pick_list/pick_list.json
@@ -51,7 +51,7 @@
"description": "Items under this warehouse will be suggested",
"fieldname": "parent_warehouse",
"fieldtype": "Link",
- "label": "Parent Warehouse",
+ "label": "Warehouse",
"options": "Warehouse"
},
{
@@ -188,7 +188,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2023-01-24 10:33:43.244476",
+ "modified": "2024-02-01 16:17:44.877426",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List",
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 758448af797..e2edb20510c 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -13,7 +13,7 @@ from frappe.model.mapper import map_child_doc
from frappe.query_builder import Case
from frappe.query_builder.custom import GROUP_CONCAT
from frappe.query_builder.functions import Coalesce, Locate, Replace, Sum
-from frappe.utils import cint, floor, flt
+from frappe.utils import ceil, cint, floor, flt
from frappe.utils.nestedset import get_descendants_of
from erpnext.selling.doctype.sales_order.sales_order import (
@@ -122,11 +122,42 @@ class PickList(Document):
def on_submit(self):
self.validate_serial_and_batch_bundle()
+ self.make_bundle_using_old_serial_batch_fields()
self.update_status()
self.update_bundle_picked_qty()
self.update_reference_qty()
self.update_sales_order_picking_status()
+ def make_bundle_using_old_serial_batch_fields(self):
+ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+ for row in self.locations:
+ if not row.serial_no and not row.batch_no:
+ continue
+
+ if not row.use_serial_batch_fields and (row.serial_no or row.batch_no):
+ frappe.throw(_("Please enable Use Old Serial / Batch Fields to make_bundle"))
+
+ if row.use_serial_batch_fields and (not row.serial_and_batch_bundle):
+ sn_doc = SerialBatchCreation(
+ {
+ "item_code": row.item_code,
+ "warehouse": row.warehouse,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "voucher_detail_no": row.name,
+ "qty": row.stock_qty,
+ "type_of_transaction": "Outward",
+ "company": self.company,
+ "serial_nos": get_serial_nos(row.serial_no) if row.serial_no else None,
+ "batches": frappe._dict({row.batch_no: row.stock_qty}) if row.batch_no else None,
+ "batch_no": row.batch_no,
+ }
+ ).make_serial_and_batch_bundle()
+
+ row.serial_and_batch_bundle = sn_doc.name
+ row.db_set("serial_and_batch_bundle", sn_doc.name)
+
def on_update_after_submit(self) -> None:
if self.has_reserved_stock():
msg = _(
@@ -156,6 +187,7 @@ class PickList(Document):
{"is_cancelled": 1, "voucher_no": ""},
)
+ frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle).cancel()
row.db_set("serial_and_batch_bundle", None)
def on_update(self):
@@ -324,7 +356,6 @@ class PickList(Document):
locations_replica = self.get("locations")
# reset
- self.remove_serial_and_batch_bundle()
self.delete_key("locations")
updated_locations = frappe._dict()
for item_doc in items:
@@ -639,13 +670,19 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus)
if not stock_qty:
break
+ serial_nos = None
+ if item_location.serial_nos:
+ serial_nos = "\n".join(item_location.serial_nos[0 : cint(stock_qty)])
+
locations.append(
frappe._dict(
{
"qty": qty,
"stock_qty": stock_qty,
"warehouse": item_location.warehouse,
- "serial_and_batch_bundle": item_location.serial_and_batch_bundle,
+ "serial_no": serial_nos,
+ "batch_no": item_location.batch_no,
+ "use_serial_batch_fields": 1,
}
)
)
@@ -681,7 +718,15 @@ def get_available_item_locations(
has_serial_no = frappe.get_cached_value("Item", item_code, "has_serial_no")
has_batch_no = frappe.get_cached_value("Item", item_code, "has_batch_no")
- if has_serial_no:
+ if has_batch_no and has_serial_no:
+ locations = get_available_item_locations_for_serial_and_batched_item(
+ item_code,
+ from_warehouses,
+ required_qty,
+ company,
+ total_picked_qty,
+ )
+ elif has_serial_no:
locations = get_available_item_locations_for_serialized_item(
item_code, from_warehouses, required_qty, company, total_picked_qty
)
@@ -724,6 +769,47 @@ def get_available_item_locations(
return locations
+def get_available_item_locations_for_serial_and_batched_item(
+ item_code,
+ from_warehouses,
+ required_qty,
+ company,
+ total_picked_qty=0,
+):
+ # Get batch nos by FIFO
+ locations = get_available_item_locations_for_batched_item(
+ item_code,
+ from_warehouses,
+ required_qty,
+ company,
+ )
+
+ if locations:
+ sn = frappe.qb.DocType("Serial No")
+ conditions = (sn.item_code == item_code) & (sn.company == company)
+
+ for location in locations:
+ location.qty = (
+ required_qty if location.qty > required_qty else location.qty
+ ) # if extra qty in batch
+
+ serial_nos = (
+ frappe.qb.from_(sn)
+ .select(sn.name)
+ .where(
+ (conditions) & (sn.batch_no == location.batch_no) & (sn.warehouse == location.warehouse)
+ )
+ .orderby(sn.creation)
+ .limit(ceil(location.qty + total_picked_qty))
+ ).run(as_dict=True)
+
+ serial_nos = [sn.name for sn in serial_nos]
+ location.serial_nos = serial_nos
+ location.qty = len(serial_nos)
+
+ return locations
+
+
def get_available_item_locations_for_serialized_item(
item_code, from_warehouses, required_qty, company, total_picked_qty=0
):
@@ -757,28 +843,16 @@ def get_available_item_locations_for_serialized_item(
picked_qty -= 1
locations = []
+
for warehouse, serial_nos in warehouse_serial_nos_map.items():
qty = len(serial_nos)
- bundle_doc = SerialBatchCreation(
- {
- "item_code": item_code,
- "warehouse": warehouse,
- "voucher_type": "Pick List",
- "total_qty": qty * -1,
- "serial_nos": serial_nos,
- "type_of_transaction": "Outward",
- "company": company,
- "do_not_submit": True,
- }
- ).make_serial_and_batch_bundle()
-
locations.append(
{
"qty": qty,
"warehouse": warehouse,
"item_code": item_code,
- "serial_and_batch_bundle": bundle_doc.name,
+ "serial_nos": serial_nos,
}
)
@@ -808,29 +882,17 @@ def get_available_item_locations_for_batched_item(
warehouse_wise_batches[d.warehouse][d.batch_no] += d.qty
for warehouse, batches in warehouse_wise_batches.items():
- qty = sum(batches.values())
-
- bundle_doc = SerialBatchCreation(
- {
- "item_code": item_code,
- "warehouse": warehouse,
- "voucher_type": "Pick List",
- "total_qty": qty * -1,
- "batches": batches,
- "type_of_transaction": "Outward",
- "company": company,
- "do_not_submit": True,
- }
- ).make_serial_and_batch_bundle()
-
- locations.append(
- {
- "qty": qty,
- "warehouse": warehouse,
- "item_code": item_code,
- "serial_and_batch_bundle": bundle_doc.name,
- }
- )
+ for batch_no, qty in batches.items():
+ locations.append(
+ frappe._dict(
+ {
+ "qty": qty,
+ "warehouse": warehouse,
+ "item_code": item_code,
+ "batch_no": batch_no,
+ }
+ )
+ )
return locations
diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py
index 322b0b46baa..cffd0d2820f 100644
--- a/erpnext/stock/doctype/pick_list/test_pick_list.py
+++ b/erpnext/stock/doctype/pick_list/test_pick_list.py
@@ -217,6 +217,8 @@ class TestPickList(FrappeTestCase):
)
pick_list.save()
+ pick_list.submit()
+
self.assertEqual(pick_list.locations[0].item_code, "_Test Serialized Item")
self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse - _TC")
self.assertEqual(pick_list.locations[0].qty, 5)
@@ -239,7 +241,7 @@ class TestPickList(FrappeTestCase):
pr1 = make_purchase_receipt(item_code="Batched Item", qty=1, rate=100.0)
pr1.load_from_db()
- oldest_batch_no = pr1.items[0].batch_no
+ oldest_batch_no = get_batch_from_bundle(pr1.items[0].serial_and_batch_bundle)
pr2 = make_purchase_receipt(item_code="Batched Item", qty=2, rate=100.0)
@@ -302,6 +304,8 @@ class TestPickList(FrappeTestCase):
}
)
pick_list.set_item_locations()
+ pick_list.submit()
+ pick_list.reload()
self.assertEqual(
get_batch_from_bundle(pick_list.locations[0].serial_and_batch_bundle), oldest_batch_no
@@ -310,6 +314,7 @@ class TestPickList(FrappeTestCase):
get_serial_nos_from_bundle(pick_list.locations[0].serial_and_batch_bundle), oldest_serial_nos
)
+ pick_list.cancel()
pr1.cancel()
pr2.cancel()
@@ -671,29 +676,22 @@ class TestPickList(FrappeTestCase):
so = make_sales_order(item_code=item, qty=25.0, rate=100)
pl = create_pick_list(so.name)
+ pl.submit()
# pick half the qty
for loc in pl.locations:
self.assertEqual(loc.qty, 25.0)
self.assertTrue(loc.serial_and_batch_bundle)
- data = frappe.get_all(
- "Serial and Batch Entry",
- fields=["qty", "batch_no"],
- filters={"parent": loc.serial_and_batch_bundle},
- )
-
- for d in data:
- self.assertEqual(d.batch_no, "PICKLT-000001")
- self.assertEqual(d.qty, 25.0 * -1)
-
pl.save()
pl.submit()
so1 = make_sales_order(item_code=item, qty=10.0, rate=100)
- pl = create_pick_list(so1.name)
+ pl1 = create_pick_list(so1.name)
+ pl1.submit()
+
# pick half the qty
- for loc in pl.locations:
- self.assertEqual(loc.qty, 10.0)
+ for loc in pl1.locations:
+ self.assertEqual(loc.qty, 5.0)
self.assertTrue(loc.serial_and_batch_bundle)
data = frappe.get_all(
@@ -709,8 +707,7 @@ class TestPickList(FrappeTestCase):
elif d.batch_no == "PICKLT-000002":
self.assertEqual(d.qty, 5.0 * -1)
- pl.save()
- pl.submit()
+ pl1.cancel()
pl.cancel()
def test_picklist_for_serial_item(self):
@@ -723,6 +720,7 @@ class TestPickList(FrappeTestCase):
so = make_sales_order(item_code=item, qty=25.0, rate=100)
pl = create_pick_list(so.name)
+ pl.submit()
picked_serial_nos = []
# pick half the qty
for loc in pl.locations:
@@ -736,13 +734,11 @@ class TestPickList(FrappeTestCase):
picked_serial_nos = [d.serial_no for d in data]
self.assertEqual(len(picked_serial_nos), 25)
- pl.save()
- pl.submit()
-
so1 = make_sales_order(item_code=item, qty=10.0, rate=100)
- pl = create_pick_list(so1.name)
+ pl1 = create_pick_list(so1.name)
+ pl1.submit()
# pick half the qty
- for loc in pl.locations:
+ for loc in pl1.locations:
self.assertEqual(loc.qty, 10.0)
self.assertTrue(loc.serial_and_batch_bundle)
@@ -756,8 +752,7 @@ class TestPickList(FrappeTestCase):
for d in data:
self.assertTrue(d.serial_no not in picked_serial_nos)
- pl.save()
- pl.submit()
+ pl1.cancel()
pl.cancel()
def test_picklist_with_bundles(self):
diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json
index e8e4afc6e3f..962fa9f09de 100644
--- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json
+++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json
@@ -24,8 +24,11 @@
"serial_no_and_batch_section",
"pick_serial_and_batch",
"serial_and_batch_bundle",
- "serial_no",
+ "use_serial_batch_fields",
"column_break_20",
+ "section_break_ecxc",
+ "serial_no",
+ "column_break_belw",
"batch_no",
"column_break_15",
"sales_order",
@@ -72,19 +75,17 @@
"read_only": 1
},
{
- "depends_on": "serial_no",
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_no",
"fieldtype": "Small Text",
- "label": "Serial No",
- "read_only": 1
+ "label": "Serial No"
},
{
- "depends_on": "batch_no",
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
"options": "Batch",
- "read_only": 1,
"search_index": 1
},
{
@@ -195,6 +196,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
@@ -204,6 +206,7 @@
"search_index": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "pick_serial_and_batch",
"fieldtype": "Button",
"label": "Pick Serial / Batch No"
@@ -218,11 +221,26 @@
"print_hide": 1,
"read_only": 1,
"report_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "use_serial_batch_fields",
+ "fieldtype": "Check",
+ "label": "Use Serial No / Batch Fields"
+ },
+ {
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
+ "fieldname": "section_break_ecxc",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_belw",
+ "fieldtype": "Column Break"
}
],
"istable": 1,
"links": [],
- "modified": "2023-07-26 12:54:15.785962",
+ "modified": "2024-02-04 16:12:16.257951",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List Item",
diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.py b/erpnext/stock/doctype/pick_list_item/pick_list_item.py
index 6e5a94e4465..f3f6298a305 100644
--- a/erpnext/stock/doctype/pick_list_item/pick_list_item.py
+++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.py
@@ -37,6 +37,7 @@ class PickListItem(Document):
stock_reserved_qty: DF.Float
stock_uom: DF.Link | None
uom: DF.Link | None
+ use_serial_batch_fields: DF.Check
warehouse: DF.Link | None
# end: auto-generated types
diff --git a/erpnext/stock/doctype/price_list/price_list.json b/erpnext/stock/doctype/price_list/price_list.json
index 56340fb05ca..38cd1ee0c66 100644
--- a/erpnext/stock/doctype/price_list/price_list.json
+++ b/erpnext/stock/doctype/price_list/price_list.json
@@ -1,434 +1,134 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
+ "actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:price_list_name",
- "beta": 0,
"creation": "2013-01-25 11:35:09",
- "custom": 0,
- "description": "Price List Master",
- "docstatus": 0,
+ "description": "A Price List is a collection of Item Prices either Selling, Buying, or both",
"doctype": "DocType",
"document_type": "Setup",
- "editable_grid": 0,
"engine": "InnoDB",
+ "field_order": [
+ "enabled",
+ "sb_1",
+ "price_list_name",
+ "currency",
+ "buying",
+ "selling",
+ "price_not_uom_dependent",
+ "column_break_3",
+ "countries"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
- "fetch_if_empty": 0,
"fieldname": "enabled",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Enabled",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Enabled"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "sb_1",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Section Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "price_list_name",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Price List Name",
- "length": 0,
"no_copy": 1,
"oldfieldname": "price_list_name",
"oldfieldtype": "Data",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
"reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
"unique": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "currency",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Currency",
- "length": 0,
- "no_copy": 0,
"options": "Currency",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
+ "default": "0",
"fieldname": "buying",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Buying",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Buying"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
+ "default": "0",
"fieldname": "selling",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Selling",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Selling"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
+ "default": "0",
"fieldname": "price_not_uom_dependent",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Price Not UOM Dependent",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Price Not UOM Dependent"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "column_break_3",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "countries",
"fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Applicable for Countries",
- "length": 0,
- "no_copy": 0,
- "options": "Price List Country",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Price List Country"
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
"icon": "fa fa-tags",
"idx": 1,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
+ "links": [],
"max_attachments": 1,
- "modified": "2019-06-24 17:16:28.027302",
+ "modified": "2024-01-30 14:39:26.328837",
"modified_by": "Administrator",
"module": "Stock",
"name": "Price List",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
"read": 1,
"report": 1,
- "role": "Sales User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
+ "role": "Sales User"
},
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
- "email": 0,
"export": 1,
- "if_owner": 0,
"import": 1,
- "permlevel": 0,
- "print": 0,
"read": 1,
"report": 1,
"role": "Sales Master Manager",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
},
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
"read": 1,
"report": 1,
- "role": "Purchase User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
+ "role": "Purchase User"
},
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
"read": 1,
"report": 1,
"role": "Purchase Master Manager",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
},
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
"read": 1,
- "report": 0,
- "role": "Manufacturing User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
+ "role": "Manufacturing User"
}
],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
"search_fields": "currency",
"show_name_in_global_search": 1,
+ "sort_field": "modified",
"sort_order": "ASC",
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 970949106c2..28d55f6ce3a 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -368,6 +368,7 @@ class PurchaseReceipt(BuyingController):
else:
self.db_set("status", "Completed")
+ self.make_bundle_using_old_serial_batch_fields()
# Updating stock ledger should always be called after updating prevdoc status,
# because updating ordered qty, reserved_qty_for_subcontract in bin
# depends upon updated ordered qty in PO
@@ -1358,16 +1359,16 @@ def get_item_account_wise_additional_cost(purchase_document):
for lcv in landed_cost_vouchers:
landed_cost_voucher_doc = frappe.get_doc("Landed Cost Voucher", lcv.parent)
+ based_on_field = None
# Use amount field for total item cost for manually cost distributed LCVs
- if landed_cost_voucher_doc.distribute_charges_based_on == "Distribute Manually":
- based_on_field = "amount"
- else:
+ if landed_cost_voucher_doc.distribute_charges_based_on != "Distribute Manually":
based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on)
total_item_cost = 0
- for item in landed_cost_voucher_doc.items:
- total_item_cost += item.get(based_on_field)
+ if based_on_field:
+ for item in landed_cost_voucher_doc.items:
+ total_item_cost += item.get(based_on_field)
for item in landed_cost_voucher_doc.items:
if item.receipt_document == purchase_document:
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index dd49eabeaf8..ff0300f9e96 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -2193,6 +2193,93 @@ class TestPurchaseReceipt(FrappeTestCase):
pr_doc.reload()
self.assertFalse(pr_doc.items[0].from_warehouse)
+ def test_use_serial_batch_fields_for_serial_nos(self):
+ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
+ create_stock_reconciliation,
+ )
+
+ item_code = make_item(
+ "_Test Use Serial Fields Item Serial Item",
+ properties={"has_serial_no": 1, "serial_no_series": "SNU-TSFISI-.#####"},
+ ).name
+
+ serial_nos = [
+ "SNU-TSFISI-000011",
+ "SNU-TSFISI-000012",
+ "SNU-TSFISI-000013",
+ "SNU-TSFISI-000014",
+ "SNU-TSFISI-000015",
+ ]
+
+ pr = make_purchase_receipt(
+ item_code=item_code,
+ qty=5,
+ serial_no="\n".join(serial_nos),
+ use_serial_batch_fields=1,
+ rate=100,
+ )
+
+ self.assertEqual(pr.items[0].use_serial_batch_fields, 1)
+ self.assertFalse(pr.items[0].serial_no)
+ self.assertTrue(pr.items[0].serial_and_batch_bundle)
+
+ sbb_doc = frappe.get_doc("Serial and Batch Bundle", pr.items[0].serial_and_batch_bundle)
+
+ for row in sbb_doc.entries:
+ self.assertTrue(row.serial_no in serial_nos)
+
+ serial_nos.remove("SNU-TSFISI-000015")
+
+ sr = create_stock_reconciliation(
+ item_code=item_code,
+ serial_no="\n".join(serial_nos),
+ qty=4,
+ warehouse=pr.items[0].warehouse,
+ use_serial_batch_fields=1,
+ do_not_submit=True,
+ )
+ sr.reload()
+
+ serial_nos = get_serial_nos(sr.items[0].current_serial_no)
+ self.assertEqual(len(serial_nos), 5)
+ self.assertEqual(sr.items[0].current_qty, 5)
+
+ new_serial_nos = get_serial_nos(sr.items[0].serial_no)
+ self.assertEqual(len(new_serial_nos), 4)
+ self.assertEqual(sr.items[0].qty, 4)
+ self.assertEqual(sr.items[0].use_serial_batch_fields, 1)
+ self.assertFalse(sr.items[0].current_serial_and_batch_bundle)
+ self.assertFalse(sr.items[0].serial_and_batch_bundle)
+ self.assertTrue(sr.items[0].current_serial_no)
+ sr.submit()
+
+ sr.reload()
+ self.assertTrue(sr.items[0].current_serial_and_batch_bundle)
+ self.assertTrue(sr.items[0].serial_and_batch_bundle)
+
+ serial_no_status = frappe.db.get_value("Serial No", "SNU-TSFISI-000015", "status")
+
+ self.assertTrue(serial_no_status != "Active")
+
+ dn = create_delivery_note(
+ item_code=item_code,
+ qty=4,
+ serial_no="\n".join(new_serial_nos),
+ use_serial_batch_fields=1,
+ )
+
+ self.assertTrue(dn.items[0].serial_and_batch_bundle)
+ self.assertEqual(dn.items[0].qty, 4)
+ doc = frappe.get_doc("Serial and Batch Bundle", dn.items[0].serial_and_batch_bundle)
+ for row in doc.entries:
+ self.assertTrue(row.serial_no in new_serial_nos)
+
+ for sn in new_serial_nos:
+ serial_no_status = frappe.db.get_value("Serial No", sn, "status")
+ self.assertTrue(serial_no_status != "Active")
+
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
@@ -2361,7 +2448,7 @@ def make_purchase_receipt(**args):
uom = args.uom or frappe.db.get_value("Item", item_code, "stock_uom") or "_Test UOM"
bundle_id = None
- if args.get("batch_no") or args.get("serial_no"):
+ if not args.use_serial_batch_fields and (args.get("batch_no") or args.get("serial_no")):
batches = {}
if args.get("batch_no"):
batches = frappe._dict({args.batch_no: qty})
@@ -2403,6 +2490,9 @@ def make_purchase_receipt(**args):
"cost_center": args.cost_center
or frappe.get_cached_value("Company", pr.company, "cost_center"),
"asset_location": args.location or "Test Location",
+ "use_serial_batch_fields": args.use_serial_batch_fields or 0,
+ "serial_no": args.serial_no if args.use_serial_batch_fields else "",
+ "batch_no": args.batch_no if args.use_serial_batch_fields else "",
},
)
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index 9bd692ad618..6b01047f006 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -94,6 +94,7 @@
"section_break_45",
"add_serial_batch_bundle",
"serial_and_batch_bundle",
+ "use_serial_batch_fields",
"col_break5",
"add_serial_batch_for_rejected_qty",
"rejected_serial_and_batch_bundle",
@@ -1003,6 +1004,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
@@ -1020,24 +1022,22 @@
{
"fieldname": "serial_no",
"fieldtype": "Text",
- "label": "Serial No",
- "read_only": 1
+ "label": "Serial No"
},
{
"fieldname": "rejected_serial_no",
"fieldtype": "Text",
- "label": "Rejected Serial No",
- "read_only": 1
+ "label": "Rejected Serial No"
},
{
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
"options": "Batch",
- "read_only": 1,
"search_index": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "rejected_serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Rejected Serial and Batch Bundle",
@@ -1045,11 +1045,13 @@
"options": "Serial and Batch Bundle"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0",
"fieldname": "add_serial_batch_for_rejected_qty",
"fieldtype": "Button",
"label": "Add Serial / Batch No (Rejected Qty)"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "section_break_3vxt",
"fieldtype": "Section Break"
},
@@ -1058,6 +1060,7 @@
"fieldtype": "Column Break"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0",
"fieldname": "add_serial_batch_bundle",
"fieldtype": "Button",
"label": "Add Serial / Batch No"
@@ -1098,12 +1101,18 @@
"read_only": 1,
"report_hide": 1,
"search_index": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "use_serial_batch_fields",
+ "fieldtype": "Check",
+ "label": "Use Serial No / Batch Fields"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2023-12-25 22:32:09.801965",
+ "modified": "2024-02-04 11:48:06.653771",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py
index aed8d21dae7..3c6dcdca488 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py
@@ -99,6 +99,7 @@ class PurchaseReceiptItem(Document):
supplier_part_no: DF.Data | None
total_weight: DF.Float
uom: DF.Link
+ use_serial_batch_fields: DF.Check
valuation_rate: DF.Currency
warehouse: DF.Link | None
weight_per_unit: DF.Float
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index 79b8ee30cfe..2715324ad0b 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -286,6 +286,7 @@ def repost(doc):
repost_gl_entries(doc)
doc.set_status("Completed")
+ remove_attached_file(doc.name)
except Exception as e:
if frappe.flags.in_test:
@@ -314,6 +315,13 @@ def repost(doc):
frappe.db.commit()
+def remove_attached_file(docname):
+ if file_name := frappe.db.get_value(
+ "File", {"attached_to_name": docname, "attached_to_doctype": "Repost Item Valuation"}, "name"
+ ):
+ frappe.delete_doc("File", file_name, delete_permanently=True)
+
+
def repost_sl_entries(doc):
if doc.based_on == "Transaction":
repost_future_sle(
diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
index aa90ff03a82..ecf9d431342 100644
--- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
@@ -424,3 +424,38 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin):
self.assertRaises(frappe.ValidationError, riv.save)
doc.cancel()
+
+ def test_remove_attached_file(self):
+ item_code = make_item("_Test Remove Attached File Item", properties={"is_stock_item": 1})
+
+ make_purchase_receipt(
+ item_code=item_code,
+ qty=1,
+ rate=100,
+ )
+
+ pr1 = make_purchase_receipt(
+ item_code=item_code,
+ qty=1,
+ rate=100,
+ posting_date=add_days(today(), days=-1),
+ )
+
+ if docname := frappe.db.exists("Repost Item Valuation", {"voucher_no": pr1.name}):
+ self.assertFalse(
+ frappe.db.get_value(
+ "File",
+ {"attached_to_doctype": "Repost Item Valuation", "attached_to_name": docname},
+ "name",
+ )
+ )
+ else:
+ repost_entries = create_item_wise_repost_entries(pr1.doctype, pr1.name)
+ for entry in repost_entries:
+ self.assertFalse(
+ frappe.db.get_value(
+ "File",
+ {"attached_to_doctype": "Repost Item Valuation", "attached_to_name": entry.name},
+ "name",
+ )
+ )
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
index 9cad8f62b88..eb4df29db82 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
@@ -1117,7 +1117,7 @@ def parse_serial_nos(data):
if isinstance(data, list):
return data
- return [s.strip() for s in cstr(data).strip().upper().replace(",", "\n").split("\n") if s.strip()]
+ return [s.strip() for s in cstr(data).strip().replace(",", "\n").split("\n") if s.strip()]
@frappe.whitelist()
@@ -1256,7 +1256,7 @@ def create_serial_batch_no_ledgers(
def get_type_of_transaction(parent_doc, child_row):
- type_of_transaction = child_row.type_of_transaction
+ type_of_transaction = child_row.get("type_of_transaction")
if parent_doc.get("doctype") == "Stock Entry":
type_of_transaction = "Outward" if child_row.s_warehouse else "Inward"
@@ -1384,6 +1384,8 @@ def get_available_serial_nos(kwargs):
filters = {"item_code": kwargs.item_code}
+ # ignore_warehouse is used for backdated stock transactions
+ # There might be chances that the serial no not exists in the warehouse during backdated stock transactions
if not kwargs.get("ignore_warehouse"):
filters["warehouse"] = ("is", "set")
if kwargs.warehouse:
@@ -1677,7 +1679,10 @@ def get_reserved_batches_for_sre(kwargs) -> dict:
query = query.where(sb_entry.batch_no == kwargs.batch_no)
if kwargs.warehouse:
- query = query.where(sre.warehouse == kwargs.warehouse)
+ if isinstance(kwargs.warehouse, list):
+ query = query.where(sre.warehouse.isin(kwargs.warehouse))
+ else:
+ query = query.where(sre.warehouse == kwargs.warehouse)
if kwargs.ignore_voucher_nos:
query = query.where(sre.name.notin(kwargs.ignore_voucher_nos))
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 0d453fb8418..f4309437086 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
@@ -136,6 +136,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
def test_old_batch_valuation(self):
frappe.flags.ignore_serial_batch_bundle_validation = True
+ frappe.flags.use_serial_and_batch_fields = True
batch_item_code = "Old Batch Item Valuation 1"
make_item(
batch_item_code,
@@ -240,6 +241,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
bundle_doc.submit()
frappe.flags.ignore_serial_batch_bundle_validation = False
+ frappe.flags.use_serial_and_batch_fields = False
def test_old_serial_no_valuation(self):
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
@@ -259,6 +261,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
)
frappe.flags.ignore_serial_batch_bundle_validation = True
+ frappe.flags.use_serial_and_batch_fields = True
serial_no_id = "Old Serial No 1"
if not frappe.db.exists("Serial No", serial_no_id):
@@ -320,6 +323,9 @@ class TestSerialandBatchBundle(FrappeTestCase):
for row in bundle_doc.entries:
self.assertEqual(flt(row.stock_value_difference, 2), -100.00)
+ frappe.flags.ignore_serial_batch_bundle_validation = False
+ frappe.flags.use_serial_and_batch_fields = False
+
def test_batch_not_belong_to_serial_no(self):
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 122664c2dde..5f4f3931a74 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -151,9 +151,7 @@ def get_serial_nos(serial_no):
if isinstance(serial_no, list):
return serial_no
- return [
- s.strip() for s in cstr(serial_no).strip().upper().replace(",", "\n").split("\n") if s.strip()
- ]
+ return [s.strip() for s in cstr(serial_no).strip().replace(",", "\n").split("\n") if s.strip()]
def clean_serial_no_string(serial_no: str) -> str:
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 00cc8be4bb8..4239191383d 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -274,6 +274,7 @@ class StockEntry(StockController):
def on_submit(self):
self.validate_closed_subcontracting_order()
+ self.make_bundle_using_old_serial_batch_fields()
self.update_stock_ledger()
self.update_work_order()
self.validate_subcontract_order()
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
index 83bfaa0094c..0f67e47ad9a 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
@@ -92,6 +92,9 @@ def make_stock_entry(**args):
else:
args.qty = cint(args.qty)
+ if args.serial_no or args.batch_no:
+ args.use_serial_batch_fields = True
+
# purpose
if not args.purpose:
if args.source and args.target:
@@ -162,6 +165,7 @@ def make_stock_entry(**args):
)
args.serial_no = serial_number
+
s.append(
"items",
{
@@ -177,6 +181,7 @@ def make_stock_entry(**args):
"batch_no": args.batch_no,
"cost_center": args.cost_center,
"expense_account": args.expense_account,
+ "use_serial_batch_fields": args.use_serial_batch_fields,
},
)
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 420afe8c4f7..7ef2a0d5a0d 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -671,6 +671,7 @@ class TestStockEntry(FrappeTestCase):
def test_serial_move(self):
se = make_serialized_item()
serial_no = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)[0]
+ frappe.flags.use_serial_and_batch_fields = True
se = frappe.copy_doc(test_records[0])
se.purpose = "Material Transfer"
@@ -691,6 +692,7 @@ class TestStockEntry(FrappeTestCase):
self.assertTrue(
frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse - _TC"
)
+ frappe.flags.use_serial_and_batch_fields = False
def test_serial_cancel(self):
se, serial_nos = self.test_serial_by_series()
@@ -990,6 +992,8 @@ class TestStockEntry(FrappeTestCase):
do_not_save=True,
)
+ frappe.flags.use_serial_and_batch_fields = True
+
cls_obj = SerialBatchCreation(
{
"type_of_transaction": "Inward",
@@ -1026,84 +1030,7 @@ class TestStockEntry(FrappeTestCase):
s2.submit()
s2.cancel()
-
- # def test_retain_sample(self):
- # from erpnext.stock.doctype.batch.batch import get_batch_qty
- # from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
-
- # create_warehouse("Test Warehouse for Sample Retention")
- # frappe.db.set_value(
- # "Stock Settings",
- # None,
- # "sample_retention_warehouse",
- # "Test Warehouse for Sample Retention - _TC",
- # )
-
- # test_item_code = "Retain Sample Item"
- # if not frappe.db.exists("Item", test_item_code):
- # item = frappe.new_doc("Item")
- # item.item_code = test_item_code
- # item.item_name = "Retain Sample Item"
- # item.description = "Retain Sample Item"
- # item.item_group = "All Item Groups"
- # item.is_stock_item = 1
- # item.has_batch_no = 1
- # item.create_new_batch = 1
- # item.retain_sample = 1
- # item.sample_quantity = 4
- # item.save()
-
- # receipt_entry = frappe.new_doc("Stock Entry")
- # receipt_entry.company = "_Test Company"
- # receipt_entry.purpose = "Material Receipt"
- # receipt_entry.append(
- # "items",
- # {
- # "item_code": test_item_code,
- # "t_warehouse": "_Test Warehouse - _TC",
- # "qty": 40,
- # "basic_rate": 12,
- # "cost_center": "_Test Cost Center - _TC",
- # "sample_quantity": 4,
- # },
- # )
- # receipt_entry.set_stock_entry_type()
- # receipt_entry.insert()
- # receipt_entry.submit()
-
- # retention_data = move_sample_to_retention_warehouse(
- # receipt_entry.company, receipt_entry.get("items")
- # )
- # retention_entry = frappe.new_doc("Stock Entry")
- # retention_entry.company = retention_data.company
- # retention_entry.purpose = retention_data.purpose
- # retention_entry.append(
- # "items",
- # {
- # "item_code": test_item_code,
- # "t_warehouse": "Test Warehouse for Sample Retention - _TC",
- # "s_warehouse": "_Test Warehouse - _TC",
- # "qty": 4,
- # "basic_rate": 12,
- # "cost_center": "_Test Cost Center - _TC",
- # "batch_no": get_batch_from_bundle(receipt_entry.get("items")[0].serial_and_batch_bundle),
- # },
- # )
- # retention_entry.set_stock_entry_type()
- # retention_entry.insert()
- # retention_entry.submit()
-
- # qty_in_usable_warehouse = get_batch_qty(
- # get_batch_from_bundle(receipt_entry.get("items")[0].serial_and_batch_bundle), "_Test Warehouse - _TC", "_Test Item"
- # )
- # qty_in_retention_warehouse = get_batch_qty(
- # get_batch_from_bundle(receipt_entry.get("items")[0].serial_and_batch_bundle),
- # "Test Warehouse for Sample Retention - _TC",
- # "_Test Item",
- # )
-
- # self.assertEqual(qty_in_usable_warehouse, 36)
- # self.assertEqual(qty_in_retention_warehouse, 4)
+ frappe.flags.use_serial_and_batch_fields = False
def test_quality_check(self):
item_code = "_Test Item For QC"
@@ -1776,6 +1703,48 @@ class TestStockEntry(FrappeTestCase):
self.assertRaises(frappe.ValidationError, se1.cancel)
+ def test_auto_reorder_level(self):
+ from erpnext.stock.reorder_item import reorder_item
+
+ item_doc = make_item(
+ "Test Auto Reorder Item - 001",
+ properties={"stock_uom": "Kg", "purchase_uom": "Nos", "is_stock_item": 1},
+ uoms=[{"uom": "Nos", "conversion_factor": 5}],
+ )
+
+ if not frappe.db.exists("Item Reorder", {"parent": item_doc.name}):
+ item_doc.append(
+ "reorder_levels",
+ {
+ "warehouse_reorder_level": 0,
+ "warehouse_reorder_qty": 10,
+ "warehouse": "_Test Warehouse - _TC",
+ "material_request_type": "Purchase",
+ },
+ )
+
+ item_doc.save(ignore_permissions=True)
+
+ frappe.db.set_single_value("Stock Settings", "auto_indent", 1)
+
+ mr_list = reorder_item()
+
+ frappe.db.set_single_value("Stock Settings", "auto_indent", 0)
+ mrs = frappe.get_all(
+ "Material Request Item",
+ fields=["qty", "stock_uom", "stock_qty"],
+ filters={"item_code": item_doc.name, "uom": "Nos"},
+ )
+
+ for mri in mrs:
+ self.assertEqual(mri.stock_uom, "Kg")
+ self.assertEqual(mri.stock_qty, 10)
+ self.assertEqual(mri.qty, 2)
+
+ for mr in mr_list:
+ mr.cancel()
+ mr.delete()
+
def make_serialized_item(**args):
args = frappe._dict(args)
diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
index bd84a2b0d99..c7b3daab82a 100644
--- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
+++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
@@ -47,9 +47,12 @@
"amount",
"serial_no_batch",
"add_serial_batch_bundle",
- "serial_and_batch_bundle",
+ "use_serial_batch_fields",
"col_break4",
+ "serial_and_batch_bundle",
+ "section_break_rdtg",
"serial_no",
+ "column_break_prps",
"batch_no",
"accounting",
"expense_account",
@@ -289,27 +292,27 @@
"no_copy": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_no",
"fieldtype": "Small Text",
"label": "Serial No",
"no_copy": 1,
"oldfieldname": "serial_no",
- "oldfieldtype": "Text",
- "read_only": 1
+ "oldfieldtype": "Text"
},
{
"fieldname": "col_break4",
"fieldtype": "Column Break"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
"no_copy": 1,
"oldfieldname": "batch_no",
"oldfieldtype": "Link",
- "options": "Batch",
- "read_only": 1
+ "options": "Batch"
},
{
"depends_on": "eval:parent.inspection_required && doc.t_warehouse",
@@ -573,24 +576,41 @@
"read_only": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0",
"fieldname": "add_serial_batch_bundle",
"fieldtype": "Button",
"label": "Add Serial / Batch No"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "use_serial_batch_fields",
+ "fieldtype": "Check",
+ "label": "Use Serial No / Batch Fields"
+ },
+ {
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
+ "fieldname": "section_break_rdtg",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_prps",
+ "fieldtype": "Column Break"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2024-01-12 11:56:04.626103",
+ "modified": "2024-02-04 16:16:47.606270",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Detail",
diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py
index a6dd0faadfc..47c443c5194 100644
--- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py
+++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py
@@ -63,6 +63,7 @@ class StockEntryDetail(Document):
transfer_qty: DF.Float
transferred_qty: DF.Float
uom: DF.Link
+ use_serial_batch_fields: DF.Check
valuation_rate: DF.Currency
# end: auto-generated types
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index 69db4f57726..75f1f2f2d98 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -93,6 +93,9 @@ class StockLedgerEntry(Document):
self.validate_inventory_dimension_negative_stock()
def validate_inventory_dimension_negative_stock(self):
+ if self.is_cancelled:
+ return
+
extra_cond = ""
kwargs = {}
diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
index d8a3f2e33c1..c0999532d03 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
@@ -482,6 +482,8 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
(item, warehouses[0], batches[1], 1, 200),
(item, warehouses[0], batches[0], 1, 200),
]
+
+ frappe.flags.use_serial_and_batch_fields = True
dns = create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list)
sle_details = fetch_sle_details_for_doc_list(dns, ["stock_value_difference"])
svd_list = [-1 * d["stock_value_difference"] for d in sle_details]
@@ -494,6 +496,8 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
"Incorrect 'Incoming Rate' values fetched for DN items",
)
+ frappe.flags.use_serial_and_batch_fields = False
+
def test_batchwise_item_valuation_stock_reco(self):
item, warehouses, batches = setup_item_valuation_test()
state = {"stock_value": 0.0, "qty": 0.0}
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
index 8e9dcb0fc52..ba7f9c58a8b 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
@@ -198,6 +198,7 @@ frappe.ui.form.on("Stock Reconciliation", {
frappe.model.set_value(cdt, cdn, "current_amount", r.message.rate * r.message.qty);
frappe.model.set_value(cdt, cdn, "amount", row.qty * row.valuation_rate);
frappe.model.set_value(cdt, cdn, "current_serial_no", r.message.serial_nos);
+ frappe.model.set_value(cdt, cdn, "use_serial_batch_fields", r.message.use_serial_batch_fields);
if (frm.doc.purpose == "Stock Reconciliation" && !frm.doc.scan_mode) {
frappe.model.set_value(cdt, cdn, "serial_no", r.message.serial_nos);
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 788ae0d3abc..ce08615ed5c 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -99,6 +99,8 @@ class StockReconciliation(StockController):
)
def on_submit(self):
+ self.make_bundle_for_current_qty()
+ self.make_bundle_using_old_serial_batch_fields()
self.update_stock_ledger()
self.make_gl_entries()
self.repost_future_sle_and_gle()
@@ -116,9 +118,52 @@ class StockReconciliation(StockController):
self.repost_future_sle_and_gle()
self.delete_auto_created_batches()
+ def make_bundle_for_current_qty(self):
+ from erpnext.stock.serial_batch_bundle import SerialBatchCreation
+
+ for row in self.items:
+ if not row.use_serial_batch_fields:
+ continue
+
+ if row.current_serial_and_batch_bundle:
+ continue
+
+ if row.current_qty and (row.current_serial_no or row.batch_no):
+ sn_doc = SerialBatchCreation(
+ {
+ "item_code": row.item_code,
+ "warehouse": row.warehouse,
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "voucher_detail_no": row.name,
+ "qty": row.qty,
+ "type_of_transaction": "Outward",
+ "company": self.company,
+ "is_rejected": 0,
+ "serial_nos": get_serial_nos(row.current_serial_no) if row.current_serial_no else None,
+ "batches": frappe._dict({row.batch_no: row.qty}) if row.batch_no else None,
+ "batch_no": row.batch_no,
+ "do_not_submit": True,
+ }
+ ).make_serial_and_batch_bundle()
+
+ row.current_serial_and_batch_bundle = sn_doc.name
+ row.db_set(
+ {
+ "current_serial_and_batch_bundle": sn_doc.name,
+ "current_serial_no": "",
+ "batch_no": "",
+ }
+ )
+
def set_current_serial_and_batch_bundle(self, voucher_detail_no=None, save=False) -> None:
"""Set Serial and Batch Bundle for each item"""
for item in self.items:
+ if not save and item.use_serial_batch_fields:
+ continue
+
if voucher_detail_no and voucher_detail_no != item.name:
continue
@@ -229,6 +274,9 @@ class StockReconciliation(StockController):
def set_new_serial_and_batch_bundle(self):
for item in self.items:
+ if item.use_serial_batch_fields:
+ continue
+
if not item.qty:
continue
@@ -291,8 +339,10 @@ class StockReconciliation(StockController):
inventory_dimensions_dict=inventory_dimensions_dict,
)
- if (item.qty is None or item.qty == item_dict.get("qty")) and (
- item.valuation_rate is None or item.valuation_rate == item_dict.get("rate")
+ if (
+ (item.qty is None or item.qty == item_dict.get("qty"))
+ and (item.valuation_rate is None or item.valuation_rate == item_dict.get("rate"))
+ and (not item.serial_no or (item.serial_no == item_dict.get("serial_nos")))
):
return False
else:
@@ -303,6 +353,11 @@ class StockReconciliation(StockController):
if item.valuation_rate is None:
item.valuation_rate = item_dict.get("rate")
+ if item_dict.get("serial_nos"):
+ item.current_serial_no = item_dict.get("serial_nos")
+ if self.purpose == "Stock Reconciliation" and not item.serial_no and item.qty:
+ item.serial_no = item.current_serial_no
+
item.current_qty = item_dict.get("qty")
item.current_valuation_rate = item_dict.get("rate")
self.calculate_difference_amount(item, item_dict)
@@ -1135,9 +1190,16 @@ def get_stock_balance_for(
has_serial_no = bool(item_dict.get("has_serial_no"))
has_batch_no = bool(item_dict.get("has_batch_no"))
+ use_serial_batch_fields = frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields")
+
if not batch_no and has_batch_no:
# Not enough information to fetch data
- return {"qty": 0, "rate": 0, "serial_nos": None}
+ return {
+ "qty": 0,
+ "rate": 0,
+ "serial_nos": None,
+ "use_serial_batch_fields": use_serial_batch_fields,
+ }
# TODO: fetch only selected batch's values
data = get_stock_balance(
@@ -1160,7 +1222,12 @@ def get_stock_balance_for(
get_batch_qty(batch_no, warehouse, posting_date=posting_date, posting_time=posting_time) or 0
)
- return {"qty": qty, "rate": rate, "serial_nos": serial_nos}
+ return {
+ "qty": qty,
+ "rate": rate,
+ "serial_nos": serial_nos,
+ "use_serial_batch_fields": use_serial_batch_fields,
+ }
@frappe.whitelist()
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 0bbfed40d89..479a74af7a8 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -1094,7 +1094,7 @@ def create_stock_reconciliation(**args):
)
bundle_id = None
- if args.batch_no or args.serial_no:
+ if not args.use_serial_batch_fields and (args.batch_no or args.serial_no):
batches = frappe._dict({})
if args.batch_no:
batches[args.batch_no] = args.qty
@@ -1125,7 +1125,10 @@ def create_stock_reconciliation(**args):
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": args.qty,
"valuation_rate": args.rate,
+ "serial_no": args.serial_no if args.use_serial_batch_fields else None,
+ "batch_no": args.batch_no if args.use_serial_batch_fields else None,
"serial_and_batch_bundle": bundle_id,
+ "use_serial_batch_fields": args.use_serial_batch_fields,
},
)
diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
index fc4ae6a5fab..734225972c7 100644
--- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
+++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
@@ -19,11 +19,14 @@
"allow_zero_valuation_rate",
"serial_no_and_batch_section",
"add_serial_batch_bundle",
- "serial_and_batch_bundle",
- "batch_no",
+ "use_serial_batch_fields",
"column_break_11",
+ "serial_and_batch_bundle",
"current_serial_and_batch_bundle",
+ "section_break_lypk",
"serial_no",
+ "column_break_eefq",
+ "batch_no",
"section_break_3",
"current_qty",
"current_amount",
@@ -103,10 +106,10 @@
"label": "Serial No and Batch"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_no",
"fieldtype": "Long Text",
- "label": "Serial No",
- "read_only": 1
+ "label": "Serial No"
},
{
"fieldname": "column_break_11",
@@ -171,11 +174,11 @@
"read_only": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
"options": "Batch",
- "read_only": 1,
"search_index": 1
},
{
@@ -195,6 +198,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial / Batch Bundle",
@@ -204,6 +208,7 @@
"search_index": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "current_serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Current Serial / Batch Bundle",
@@ -212,6 +217,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "add_serial_batch_bundle",
"fieldtype": "Button",
"label": "Add Serial / Batch No"
@@ -222,11 +228,26 @@
"fieldtype": "Link",
"label": "Item Group",
"options": "Item Group"
+ },
+ {
+ "default": "0",
+ "fieldname": "use_serial_batch_fields",
+ "fieldtype": "Check",
+ "label": "Use Serial No / Batch Fields"
+ },
+ {
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
+ "fieldname": "section_break_lypk",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_eefq",
+ "fieldtype": "Column Break"
}
],
"istable": 1,
"links": [],
- "modified": "2024-01-14 10:04:23.599951",
+ "modified": "2024-02-04 16:19:44.576022",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation Item",
diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.py b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.py
index c82cdf58de1..1938fec32b0 100644
--- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.py
+++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.py
@@ -26,6 +26,7 @@ class StockReconciliationItem(Document):
current_valuation_rate: DF.Currency
has_item_scanned: DF.Data | None
item_code: DF.Link
+ item_group: DF.Link | None
item_name: DF.Data | None
parent: DF.Data
parentfield: DF.Data
@@ -34,6 +35,7 @@ class StockReconciliationItem(Document):
quantity_difference: DF.ReadOnly | None
serial_and_batch_bundle: DF.Link | None
serial_no: DF.LongText | None
+ use_serial_batch_fields: DF.Check
valuation_rate: DF.Currency
warehouse: DF.Link
# end: auto-generated types
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json
index 122829032de..3f2c1142552 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.json
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.json
@@ -1,7 +1,7 @@
{
"actions": [],
"creation": "2013-06-24 16:37:54",
- "description": "Settings",
+ "description": "Default settings for your stock-related transactions",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
@@ -50,6 +50,7 @@
"disable_serial_no_and_batch_selector",
"use_naming_series",
"naming_series_prefix",
+ "use_serial_batch_fields",
"stock_planning_tab",
"auto_material_request",
"auto_indent",
@@ -420,6 +421,12 @@
"fieldname": "auto_reserve_stock_for_sales_order_on_purchase",
"fieldtype": "Check",
"label": "Auto Reserve Stock for Sales Order on Purchase"
+ },
+ {
+ "default": "1",
+ "fieldname": "use_serial_batch_fields",
+ "fieldtype": "Check",
+ "label": "Use Serial / Batch Fields"
}
],
"icon": "icon-cog",
@@ -427,7 +434,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2023-10-18 12:35:30.068799",
+ "modified": "2024-02-04 12:01:31.931864",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py
index 088c7cdfe1c..c4960aa67a8 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.py
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.py
@@ -57,6 +57,7 @@ class StockSettings(Document):
stock_uom: DF.Link | None
update_existing_price_list_rate: DF.Check
use_naming_series: DF.Check
+ use_serial_batch_fields: DF.Check
valuation_method: DF.Literal["FIFO", "Moving Average", "LIFO"]
# end: auto-generated types
@@ -68,6 +69,7 @@ class StockSettings(Document):
"allow_negative_stock",
"default_warehouse",
"set_qty_in_transactions_based_on_serial_no_input",
+ "use_serial_batch_fields",
]:
frappe.db.set_default(key, self.get(key, ""))
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index e7465959212..1cb10575cd1 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -86,7 +86,8 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
get_party_item_code(args, item, out)
- set_valuation_rate(out, args)
+ if args.get("doctype") in ["Sales Order", "Quotation"]:
+ set_valuation_rate(out, args)
update_party_blanket_order(args, out)
@@ -269,7 +270,9 @@ def get_basic_details(args, item, overwrite_warehouse=True):
if not item:
item = frappe.get_doc("Item", args.get("item_code"))
- if item.variant_of and not item.taxes:
+ if (
+ item.variant_of and not item.taxes and frappe.db.exists("Item Tax", {"parent": item.variant_of})
+ ):
item.update_template_tables()
item_defaults = get_item_defaults(item.name, args.company)
@@ -497,8 +500,8 @@ def update_barcode_value(out):
def get_barcode_data(items_list):
- # get itemwise batch no data
- # exmaple: {'LED-GRE': [Batch001, Batch002]}
+ # get item-wise batch no data
+ # example: {'LED-GRE': [Batch001, Batch002]}
# where LED-GRE is item code, SN0001 is serial no and Pune is warehouse
itemwise_barcode = {}
@@ -543,7 +546,7 @@ def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_t
args = {
"company": company,
"tax_category": tax_category,
- "net_rate": item_rates.get(item_code[1]),
+ "base_net_rate": item_rates.get(item_code[1]),
}
if item_tax_templates:
@@ -635,7 +638,7 @@ def is_within_valid_range(args, tax):
if not flt(tax.maximum_net_rate):
# No range specified, just ignore
return True
- elif flt(tax.minimum_net_rate) <= flt(args.get("net_rate")) <= flt(tax.maximum_net_rate):
+ elif flt(tax.minimum_net_rate) <= flt(args.get("base_net_rate")) <= flt(tax.maximum_net_rate):
return True
return False
diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py
index caa3d66ac39..51913005afd 100644
--- a/erpnext/stock/reorder_item.py
+++ b/erpnext/stock/reorder_item.py
@@ -34,73 +34,157 @@ def _reorder_item():
erpnext.get_default_company() or frappe.db.sql("""select name from tabCompany limit 1""")[0][0]
)
- items_to_consider = frappe.db.sql_list(
- """select name from `tabItem` item
- where is_stock_item=1 and has_variants=0
- and disabled=0
- and (end_of_life is null or end_of_life='0000-00-00' or end_of_life > %(today)s)
- and (exists (select name from `tabItem Reorder` ir where ir.parent=item.name)
- or (variant_of is not null and variant_of != ''
- and exists (select name from `tabItem Reorder` ir where ir.parent=item.variant_of))
- )""",
- {"today": nowdate()},
- )
+ items_to_consider = get_items_for_reorder()
if not items_to_consider:
return
item_warehouse_projected_qty = get_item_warehouse_projected_qty(items_to_consider)
- def add_to_material_request(
- item_code, warehouse, reorder_level, reorder_qty, material_request_type, warehouse_group=None
- ):
- if warehouse not in warehouse_company:
+ def add_to_material_request(**kwargs):
+ if isinstance(kwargs, dict):
+ kwargs = frappe._dict(kwargs)
+
+ if kwargs.warehouse not in warehouse_company:
# a disabled warehouse
return
- reorder_level = flt(reorder_level)
- reorder_qty = flt(reorder_qty)
+ reorder_level = flt(kwargs.reorder_level)
+ reorder_qty = flt(kwargs.reorder_qty)
# projected_qty will be 0 if Bin does not exist
- if warehouse_group:
- projected_qty = flt(item_warehouse_projected_qty.get(item_code, {}).get(warehouse_group))
+ if kwargs.warehouse_group:
+ projected_qty = flt(
+ item_warehouse_projected_qty.get(kwargs.item_code, {}).get(kwargs.warehouse_group)
+ )
else:
- projected_qty = flt(item_warehouse_projected_qty.get(item_code, {}).get(warehouse))
+ projected_qty = flt(
+ item_warehouse_projected_qty.get(kwargs.item_code, {}).get(kwargs.warehouse)
+ )
if (reorder_level or reorder_qty) and projected_qty <= reorder_level:
deficiency = reorder_level - projected_qty
if deficiency > reorder_qty:
reorder_qty = deficiency
- company = warehouse_company.get(warehouse) or default_company
+ company = warehouse_company.get(kwargs.warehouse) or default_company
- material_requests[material_request_type].setdefault(company, []).append(
- {"item_code": item_code, "warehouse": warehouse, "reorder_qty": reorder_qty}
+ material_requests[kwargs.material_request_type].setdefault(company, []).append(
+ {
+ "item_code": kwargs.item_code,
+ "warehouse": kwargs.warehouse,
+ "reorder_qty": reorder_qty,
+ "item_details": kwargs.item_details,
+ }
)
- for item_code in items_to_consider:
- item = frappe.get_doc("Item", item_code)
+ for item_code, reorder_levels in items_to_consider.items():
+ for d in reorder_levels:
+ if d.has_variants:
+ continue
- if item.variant_of and not item.get("reorder_levels"):
- item.update_template_tables()
-
- if item.get("reorder_levels"):
- for d in item.get("reorder_levels"):
- add_to_material_request(
- item_code,
- d.warehouse,
- d.warehouse_reorder_level,
- d.warehouse_reorder_qty,
- d.material_request_type,
- warehouse_group=d.warehouse_group,
- )
+ add_to_material_request(
+ item_code=item_code,
+ warehouse=d.warehouse,
+ reorder_level=d.warehouse_reorder_level,
+ reorder_qty=d.warehouse_reorder_qty,
+ material_request_type=d.material_request_type,
+ warehouse_group=d.warehouse_group,
+ item_details=frappe._dict(
+ {
+ "item_code": item_code,
+ "name": item_code,
+ "item_name": d.item_name,
+ "item_group": d.item_group,
+ "brand": d.brand,
+ "description": d.description,
+ "stock_uom": d.stock_uom,
+ "purchase_uom": d.purchase_uom,
+ }
+ ),
+ )
if material_requests:
return create_material_request(material_requests)
+def get_items_for_reorder() -> dict[str, list]:
+ reorder_table = frappe.qb.DocType("Item Reorder")
+ item_table = frappe.qb.DocType("Item")
+
+ query = (
+ frappe.qb.from_(reorder_table)
+ .inner_join(item_table)
+ .on(reorder_table.parent == item_table.name)
+ .select(
+ reorder_table.warehouse,
+ reorder_table.warehouse_group,
+ reorder_table.material_request_type,
+ reorder_table.warehouse_reorder_level,
+ reorder_table.warehouse_reorder_qty,
+ item_table.name,
+ item_table.stock_uom,
+ item_table.purchase_uom,
+ item_table.description,
+ item_table.item_name,
+ item_table.item_group,
+ item_table.brand,
+ item_table.variant_of,
+ item_table.has_variants,
+ )
+ .where(
+ (item_table.disabled == 0)
+ & (item_table.is_stock_item == 1)
+ & (
+ (item_table.end_of_life.isnull())
+ | (item_table.end_of_life > nowdate())
+ | (item_table.end_of_life == "0000-00-00")
+ )
+ )
+ )
+
+ data = query.run(as_dict=True)
+ itemwise_reorder = frappe._dict({})
+ for d in data:
+ itemwise_reorder.setdefault(d.name, []).append(d)
+
+ itemwise_reorder = get_reorder_levels_for_variants(itemwise_reorder)
+
+ return itemwise_reorder
+
+
+def get_reorder_levels_for_variants(itemwise_reorder):
+ item_table = frappe.qb.DocType("Item")
+
+ query = (
+ frappe.qb.from_(item_table)
+ .select(
+ item_table.name,
+ item_table.variant_of,
+ )
+ .where(
+ (item_table.disabled == 0)
+ & (item_table.is_stock_item == 1)
+ & (
+ (item_table.end_of_life.isnull())
+ | (item_table.end_of_life > nowdate())
+ | (item_table.end_of_life == "0000-00-00")
+ )
+ & (item_table.variant_of.notnull())
+ )
+ )
+
+ variants_item = query.run(as_dict=True)
+ for row in variants_item:
+ if not itemwise_reorder.get(row.name) and itemwise_reorder.get(row.variant_of):
+ itemwise_reorder.setdefault(row.name, []).extend(itemwise_reorder.get(row.variant_of, []))
+
+ return itemwise_reorder
+
+
def get_item_warehouse_projected_qty(items_to_consider):
item_warehouse_projected_qty = {}
+ items_to_consider = list(items_to_consider.keys())
for item_code, warehouse, projected_qty in frappe.db.sql(
"""select item_code, warehouse, projected_qty
@@ -164,7 +248,7 @@ def create_material_request(material_requests):
for d in items:
d = frappe._dict(d)
- item = frappe.get_doc("Item", d.item_code)
+ item = d.get("item_details")
uom = item.stock_uom
conversion_factor = 1.0
@@ -190,6 +274,7 @@ def create_material_request(material_requests):
"item_code": d.item_code,
"schedule_date": add_days(nowdate(), cint(item.lead_time_days)),
"qty": qty,
+ "conversion_factor": conversion_factor,
"uom": uom,
"stock_uom": item.stock_uom,
"warehouse": d.warehouse,
diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py
index ed84a5c2d5a..269323810b4 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.py
+++ b/erpnext/stock/report/stock_balance/stock_balance.py
@@ -90,8 +90,7 @@ class StockBalanceReport(object):
self.opening_data.setdefault(group_by_key, entry)
def prepare_new_data(self):
- if not self.sle_entries:
- return
+ self.item_warehouse_map = self.get_item_warehouse_map()
if self.filters.get("show_stock_ageing_data"):
self.filters["show_warehouse_wise_stock"] = True
@@ -99,7 +98,8 @@ class StockBalanceReport(object):
_func = itemgetter(1)
- self.item_warehouse_map = self.get_item_warehouse_map()
+ del self.sle_entries
+
sre_details = self.get_sre_reserved_qty_details()
variant_values = {}
@@ -143,15 +143,22 @@ class StockBalanceReport(object):
item_warehouse_map = {}
self.opening_vouchers = self.get_opening_vouchers()
- for entry in self.sle_entries:
- group_by_key = self.get_group_by_key(entry)
- if group_by_key not in item_warehouse_map:
- self.initialize_data(item_warehouse_map, group_by_key, entry)
+ if self.filters.get("show_stock_ageing_data"):
+ self.sle_entries = self.sle_query.run(as_dict=True)
- self.prepare_item_warehouse_map(item_warehouse_map, entry, group_by_key)
+ with frappe.db.unbuffered_cursor():
+ if not self.filters.get("show_stock_ageing_data"):
+ self.sle_entries = self.sle_query.run(as_dict=True, as_iterator=True)
- if self.opening_data.get(group_by_key):
- del self.opening_data[group_by_key]
+ for entry in self.sle_entries:
+ group_by_key = self.get_group_by_key(entry)
+ if group_by_key not in item_warehouse_map:
+ self.initialize_data(item_warehouse_map, group_by_key, entry)
+
+ self.prepare_item_warehouse_map(item_warehouse_map, entry, group_by_key)
+
+ if self.opening_data.get(group_by_key):
+ del self.opening_data[group_by_key]
for group_by_key, entry in self.opening_data.items():
if group_by_key not in item_warehouse_map:
@@ -252,7 +259,8 @@ class StockBalanceReport(object):
.where(
(table.docstatus == 1)
& (table.company == self.filters.company)
- & ((table.to_date <= self.from_date))
+ & (table.to_date <= self.from_date)
+ & (table.status == "Completed")
)
.orderby(table.to_date, order=Order.desc)
.limit(1)
@@ -305,7 +313,7 @@ class StockBalanceReport(object):
if self.filters.get("company"):
query = query.where(sle.company == self.filters.get("company"))
- self.sle_entries = query.run(as_dict=True)
+ self.sle_query = query
def apply_inventory_dimensions_filters(self, query, sle) -> str:
inventory_dimension_fields = self.get_inventory_dimension_fields()
diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
index 4cfe5d817e6..d8b5b34d449 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -283,6 +283,7 @@ class SerialBatchBundle:
if (sn_table.purchase_document_no != self.sle.voucher_no and self.sle.is_cancelled != 1)
else "Inactive",
)
+ .set(sn_table.company, self.sle.company)
.where(sn_table.name.isin(serial_nos))
).run()
@@ -793,6 +794,9 @@ class SerialBatchCreation:
setattr(self, "actual_qty", qty)
self.__dict__["actual_qty"] = self.actual_qty
+ if not hasattr(self, "use_serial_batch_fields"):
+ setattr(self, "use_serial_batch_fields", 0)
+
def duplicate_package(self):
if not self.serial_and_batch_bundle:
return
@@ -901,9 +905,14 @@ class SerialBatchCreation:
self.batches = get_available_batches(kwargs)
def set_auto_serial_batch_entries_for_inward(self):
+ print(self.get("serial_nos"))
+
if (self.get("batches") and self.has_batch_no) or (
self.get("serial_nos") and self.has_serial_no
):
+ if self.use_serial_batch_fields and self.get("serial_nos"):
+ self.make_serial_no_if_not_exists()
+
return
self.batch_no = None
@@ -915,6 +924,59 @@ class SerialBatchCreation:
else:
self.batches = frappe._dict({self.batch_no: abs(self.actual_qty)})
+ def make_serial_no_if_not_exists(self):
+ non_exists_serial_nos = []
+ for row in self.serial_nos:
+ if not frappe.db.exists("Serial No", row):
+ non_exists_serial_nos.append(row)
+
+ if non_exists_serial_nos:
+ self.make_serial_nos(non_exists_serial_nos)
+
+ def make_serial_nos(self, serial_nos):
+ serial_nos_details = []
+ batch_no = None
+ if self.batches:
+ batch_no = list(self.batches.keys())[0]
+
+ for serial_no in serial_nos:
+ serial_nos_details.append(
+ (
+ serial_no,
+ serial_no,
+ now(),
+ now(),
+ frappe.session.user,
+ frappe.session.user,
+ self.warehouse,
+ self.company,
+ self.item_code,
+ self.item_name,
+ self.description,
+ "Active",
+ batch_no,
+ )
+ )
+
+ if serial_nos_details:
+ fields = [
+ "name",
+ "serial_no",
+ "creation",
+ "modified",
+ "owner",
+ "modified_by",
+ "warehouse",
+ "company",
+ "item_code",
+ "item_name",
+ "description",
+ "status",
+ "batch_no",
+ ]
+
+ frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
+
def set_serial_batch_entries(self, doc):
if self.get("serial_nos"):
serial_no_wise_batch = frappe._dict({})
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 0a6a686d8e3..379320237e6 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -897,9 +897,12 @@ class update_entries_after(object):
self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount)
- self.wh_data.qty_after_transaction += doc.total_qty
+ precision = doc.precision("total_qty")
+ self.wh_data.qty_after_transaction += flt(doc.total_qty, precision)
if self.wh_data.qty_after_transaction:
- self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
+ self.wh_data.valuation_rate = flt(self.wh_data.stock_value, precision) / flt(
+ self.wh_data.qty_after_transaction, precision
+ )
def validate_negative_stock(self, sle):
"""
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 017db5d5505..54e0ab5acf8 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -11,6 +11,9 @@ from frappe.query_builder.functions import CombineDatetime, IfNull, Sum
from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime
import erpnext
+from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
+ get_available_serial_nos,
+)
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
from erpnext.stock.valuation import FIFOValuation, LIFOValuation
@@ -125,7 +128,21 @@ def get_stock_balance(
if with_valuation_rate:
if with_serial_no:
- serial_nos = get_serial_nos_data_after_transactions(args)
+ serial_no_details = get_available_serial_nos(
+ frappe._dict(
+ {
+ "item_code": item_code,
+ "warehouse": warehouse,
+ "posting_date": posting_date,
+ "posting_time": posting_time,
+ "ignore_warehouse": 1,
+ }
+ )
+ )
+
+ serial_nos = ""
+ if serial_no_details:
+ serial_nos = "\n".join(d.serial_no for d in serial_no_details)
return (
(last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos)
@@ -140,38 +157,6 @@ def get_stock_balance(
return last_entry.qty_after_transaction if last_entry else 0.0
-def get_serial_nos_data_after_transactions(args):
-
- serial_nos = set()
- args = frappe._dict(args)
- sle = frappe.qb.DocType("Stock Ledger Entry")
-
- stock_ledger_entries = (
- frappe.qb.from_(sle)
- .select("serial_no", "actual_qty")
- .where(
- (sle.item_code == args.item_code)
- & (sle.warehouse == args.warehouse)
- & (
- CombineDatetime(sle.posting_date, sle.posting_time)
- < CombineDatetime(args.posting_date, args.posting_time)
- )
- & (sle.is_cancelled == 0)
- )
- .orderby(sle.posting_date, sle.posting_time, sle.creation)
- .run(as_dict=1)
- )
-
- for stock_ledger_entry in stock_ledger_entries:
- changed_serial_no = get_serial_nos_data(stock_ledger_entry.serial_no)
- if stock_ledger_entry.actual_qty > 0:
- serial_nos.update(changed_serial_no)
- else:
- serial_nos.difference_update(changed_serial_no)
-
- return "\n".join(serial_nos)
-
-
def get_serial_nos_data(serial_nos):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
index 475b6030780..8d82709e75f 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
@@ -149,6 +149,7 @@ class SubcontractingReceipt(SubcontractingController):
self.update_prevdoc_status()
self.set_subcontracting_order_status()
self.set_consumed_qty_in_subcontract_order()
+ self.make_bundle_using_old_serial_batch_fields()
self.update_stock_ledger()
self.make_gl_entries()
self.repost_future_sle_and_gle()
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json
index 9bfc2fdb7a1..f9e0a0b591c 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json
@@ -48,11 +48,14 @@
"reference_name",
"section_break_45",
"serial_and_batch_bundle",
- "serial_no",
+ "use_serial_batch_fields",
"col_break5",
"rejected_serial_and_batch_bundle",
- "batch_no",
+ "section_break_jshh",
+ "serial_no",
"rejected_serial_no",
+ "column_break_henr",
+ "batch_no",
"manufacture_details",
"manufacturer",
"column_break_16",
@@ -311,22 +314,20 @@
"label": "Serial and Batch Details"
},
{
- "depends_on": "eval:!doc.is_fixed_asset",
+ "depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1",
"fieldname": "serial_no",
"fieldtype": "Small Text",
"label": "Serial No",
- "no_copy": 1,
- "read_only": 1
+ "no_copy": 1
},
{
- "depends_on": "eval:!doc.is_fixed_asset",
+ "depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
"no_copy": 1,
"options": "Batch",
- "print_hide": 1,
- "read_only": 1
+ "print_hide": 1
},
{
"depends_on": "eval: !parent.is_return",
@@ -478,6 +479,7 @@
"label": "Accounting Details"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
@@ -486,6 +488,7 @@
"print_hide": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "rejected_serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Rejected Serial and Batch Bundle",
@@ -546,12 +549,27 @@
"fieldtype": "Check",
"label": "Include Exploded Items",
"print_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "use_serial_batch_fields",
+ "fieldtype": "Check",
+ "label": "Use Serial No / Batch Fields"
+ },
+ {
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
+ "fieldname": "section_break_jshh",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_henr",
+ "fieldtype": "Column Break"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2023-11-30 12:05:51.920705",
+ "modified": "2024-02-04 16:23:30.374865",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Receipt Item",
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py
index d02160ece45..1a4ce5b977a 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py
@@ -58,6 +58,7 @@ class SubcontractingReceiptItem(Document):
subcontracting_order: DF.Link | None
subcontracting_order_item: DF.Data | None
subcontracting_receipt_item: DF.Data | None
+ use_serial_batch_fields: DF.Check
warehouse: DF.Link | None
# end: auto-generated types
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json
index 90bcf4e544e..957b6a2a654 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json
@@ -26,10 +26,13 @@
"current_stock",
"secbreak_3",
"serial_and_batch_bundle",
- "batch_no",
+ "use_serial_batch_fields",
"col_break4",
+ "subcontracting_order",
+ "section_break_zwnh",
"serial_no",
- "subcontracting_order"
+ "column_break_qibi",
+ "batch_no"
],
"fields": [
{
@@ -60,19 +63,19 @@
"width": "300px"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
"no_copy": 1,
- "options": "Batch",
- "read_only": 1
+ "options": "Batch"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_no",
"fieldtype": "Text",
"label": "Serial No",
- "no_copy": 1,
- "read_only": 1
+ "no_copy": 1
},
{
"fieldname": "col_break1",
@@ -198,6 +201,7 @@
},
{
"columns": 2,
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"in_list_view": 1,
@@ -205,12 +209,27 @@
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "use_serial_batch_fields",
+ "fieldtype": "Check",
+ "label": "Use Serial No / Batch Fields"
+ },
+ {
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
+ "fieldname": "section_break_zwnh",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_qibi",
+ "fieldtype": "Column Break"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2023-03-15 13:55:08.132626",
+ "modified": "2024-02-04 16:32:17.534162",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Receipt Supplied Item",
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py
index 2ee55518d52..8f09197aa83 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py
@@ -35,6 +35,7 @@ class SubcontractingReceiptSuppliedItem(Document):
serial_no: DF.Text | None
stock_uom: DF.Link | None
subcontracting_order: DF.Link | None
+ use_serial_batch_fields: DF.Check
# end: auto-generated types
pass
diff --git a/erpnext/templates/pages/order.html b/erpnext/templates/pages/order.html
index 97bf48727cf..6c59a9688dc 100644
--- a/erpnext/templates/pages/order.html
+++ b/erpnext/templates/pages/order.html
@@ -34,6 +34,18 @@
+ {% if show_pay_button %}
+
+
+
+ {% endif %}
{% endblock %}
diff --git a/erpnext/templates/pages/order.py b/erpnext/templates/pages/order.py
index d0968bf88a2..21d4b860d1f 100644
--- a/erpnext/templates/pages/order.py
+++ b/erpnext/templates/pages/order.py
@@ -48,7 +48,10 @@ def get_context(context):
)
context.available_loyalty_points = int(loyalty_program_details.get("loyalty_points"))
- context.show_pay_button = frappe.db.get_single_value("Buying Settings", "show_pay_button")
+ context.show_pay_button = (
+ "payments" in frappe.get_installed_apps()
+ and frappe.db.get_single_value("Buying Settings", "show_pay_button")
+ )
context.show_make_pi_button = False
if context.doc.get("supplier"):
# show Make Purchase Invoice button based on permission