diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/hu_chart_of_accounts_for_microenterprises_with_account_number.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/hu_chart_of_accounts_for_microenterprises_with_account_number.json
index 2cd6c0fc61a..c8c9e76440a 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/hu_chart_of_accounts_for_microenterprises_with_account_number.json
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/hu_chart_of_accounts_for_microenterprises_with_account_number.json
@@ -1,4 +1,6 @@
{
+ "country_code": "hu",
+ "name": "Hungary - Chart of Accounts for Microenterprises",
"tree": {
"SZ\u00c1MLAOSZT\u00c1LY BEFEKTETETT ESZK\u00d6Z\u00d6K": {
"account_number": 1,
diff --git a/erpnext/accounts/doctype/allowed_to_transact_with/allowed_to_transact_with.json b/erpnext/accounts/doctype/allowed_to_transact_with/allowed_to_transact_with.json
index e3f2d59c065..234ffc8a870 100644
--- a/erpnext/accounts/doctype/allowed_to_transact_with/allowed_to_transact_with.json
+++ b/erpnext/accounts/doctype/allowed_to_transact_with/allowed_to_transact_with.json
@@ -11,6 +11,7 @@
{
"fieldname": "company",
"fieldtype": "Link",
+ "ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Company",
"options": "Company",
@@ -19,7 +20,7 @@
],
"istable": 1,
"links": [],
- "modified": "2020-05-01 12:32:34.044911",
+ "modified": "2024-01-03 11:13:02.669632",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Allowed To Transact With",
@@ -28,5 +29,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
index 0779a09e2f4..9e6b51d2c18 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -444,6 +444,10 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
vouchers = json.loads(vouchers)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
transaction.add_payment_entries(vouchers)
+ transaction.validate_duplicate_references()
+ transaction.allocate_payment_entries()
+ transaction.update_allocated_amount()
+ transaction.set_status()
transaction.save()
return transaction
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index 629ed1cf751..1d6cb8e2c09 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -3,12 +3,11 @@
import frappe
from frappe import _
+from frappe.model.document import Document
from frappe.utils import flt
-from erpnext.controllers.status_updater import StatusUpdater
-
-class BankTransaction(StatusUpdater):
+class BankTransaction(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
@@ -50,6 +49,15 @@ class BankTransaction(StatusUpdater):
def validate(self):
self.validate_duplicate_references()
+ def set_status(self):
+ if self.docstatus == 2:
+ self.db_set("status", "Cancelled")
+ elif self.docstatus == 1:
+ if self.unallocated_amount > 0:
+ self.db_set("status", "Unreconciled")
+ elif self.unallocated_amount <= 0:
+ self.db_set("status", "Reconciled")
+
def validate_duplicate_references(self):
"""Make sure the same voucher is not allocated twice within the same Bank Transaction"""
if not self.payment_entries:
@@ -83,12 +91,13 @@ class BankTransaction(StatusUpdater):
self.validate_duplicate_references()
self.allocate_payment_entries()
self.update_allocated_amount()
+ self.set_status()
def on_cancel(self):
for payment_entry in self.payment_entries:
self.clear_linked_payment_entry(payment_entry, for_cancel=True)
- self.set_status(update=True)
+ self.set_status()
def add_payment_entries(self, vouchers):
"Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
@@ -366,15 +375,17 @@ def set_voucher_clearance(doctype, docname, clearance_date, self):
and len(get_reconciled_bank_transactions(doctype, docname)) < 2
):
return
- frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
- elif doctype == "Sales Invoice":
- frappe.db.set_value(
- "Sales Invoice Payment",
- dict(parenttype=doctype, parent=docname),
- "clearance_date",
- clearance_date,
- )
+ if doctype == "Sales Invoice":
+ frappe.db.set_value(
+ "Sales Invoice Payment",
+ dict(parenttype=doctype, parent=docname),
+ "clearance_date",
+ clearance_date,
+ )
+ return
+
+ frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
elif doctype == "Bank Transaction":
# For when a second bank transaction has fixed another, e.g. refund
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 26112409b7c..81ffee3f6e8 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -747,6 +747,10 @@ frappe.ui.form.on('Payment Entry', {
args["get_orders_to_be_billed"] = true;
}
+ if (frm.doc.book_advance_payments_in_separate_party_account) {
+ args["book_advance_payments_in_separate_party_account"] = true;
+ }
+
frappe.flags.allocate_payment_amount = filters['allocate_payment_amount'];
return frappe.call({
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 1282ab60392..e20da1d9d62 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -256,6 +256,7 @@ class PaymentEntry(AccountsController):
"get_outstanding_invoices": True,
"get_orders_to_be_billed": True,
"vouchers": vouchers,
+ "book_advance_payments_in_separate_party_account": self.book_advance_payments_in_separate_party_account,
},
validate=True,
)
@@ -1614,11 +1615,16 @@ def get_outstanding_reference_documents(args, validate=False):
outstanding_invoices = []
negative_outstanding_invoices = []
+ if args.get("book_advance_payments_in_separate_party_account"):
+ party_account = get_party_account(args.get("party_type"), args.get("party"), args.get("company"))
+ else:
+ party_account = args.get("party_account")
+
if args.get("get_outstanding_invoices"):
outstanding_invoices = get_outstanding_invoices(
args.get("party_type"),
args.get("party"),
- get_party_account(args.get("party_type"), args.get("party"), args.get("company")),
+ party_account,
common_filter=common_filter,
posting_date=posting_and_due_date,
min_outstanding=args.get("outstanding_amt_greater_than"),
diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py
index 18aa6820a38..17293adb95e 100644
--- a/erpnext/accounts/doctype/pricing_rule/utils.py
+++ b/erpnext/accounts/doctype/pricing_rule/utils.py
@@ -527,7 +527,7 @@ def get_qty_amount_data_for_cumulative(pr_doc, doc, items=None):
values.extend(warehouses)
if items:
- condition = " and `tab{child_doc}`.{apply_on} in ({items})".format(
+ condition += " and `tab{child_doc}`.{apply_on} in ({items})".format(
child_doc=child_doctype, apply_on=apply_on, items=",".join(["%s"] * len(items))
)
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 657723796cf..aa52600a889 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -1084,17 +1084,6 @@ class PurchaseInvoice(BuyingController):
item=item,
)
)
-
- # update gross amount of asset bought through this document
- assets = frappe.db.get_all(
- "Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
- )
- for asset in assets:
- frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
- frappe.db.set_value(
- "Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate)
- )
-
if (
self.auto_accounting_for_stock
and self.is_opening == "No"
@@ -1134,17 +1123,24 @@ class PurchaseInvoice(BuyingController):
item.item_tax_amount, item.precision("item_tax_amount")
)
+ if item.is_fixed_asset and item.landed_cost_voucher_amount:
+ self.update_gross_purchase_amount_for_linked_assets(item)
+
+ def update_gross_purchase_amount_for_linked_assets(self, item):
assets = frappe.db.get_all(
"Asset",
filters={"purchase_invoice": self.name, "item_code": item.item_code},
fields=["name", "asset_quantity"],
)
for asset in assets:
+ purchase_amount = flt(item.valuation_rate) * asset.asset_quantity
frappe.db.set_value(
- "Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate) * asset.asset_quantity
- )
- frappe.db.set_value(
- "Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate) * asset.asset_quantity
+ "Asset",
+ asset.name,
+ {
+ "gross_purchase_amount": purchase_amount,
+ "purchase_receipt_amount": purchase_amount,
+ },
)
def make_stock_adjustment_entry(
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index e41cec7eee3..981add74a65 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -1227,11 +1227,11 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
def test_gain_loss_with_advance_entry(self):
- unlink_enabled = frappe.db.get_value(
- "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice"
+ unlink_enabled = frappe.db.get_single_value(
+ "Accounts Settings", "unlink_payment_on_cancellation_of_invoice"
)
- frappe.db.set_single_value("Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1)
+ frappe.db.set_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice", 1)
original_account = frappe.db.get_value("Company", "_Test Company", "exchange_gain_loss_account")
frappe.db.set_value(
@@ -1422,7 +1422,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
pay.cancel()
frappe.db.set_single_value(
- "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled
+ "Accounts Settings", "unlink_payment_on_cancellation_of_invoice", unlink_enabled
)
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index c8d92d0d705..ba2cd82516f 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -898,8 +898,8 @@ frappe.ui.form.on('Sales Invoice', {
frm.events.append_time_log(frm, timesheet, 1.0);
}
});
- frm.refresh_field("timesheets");
frm.trigger("calculate_timesheet_totals");
+ frm.refresh();
},
async get_exchange_rate(frm, from_currency, to_currency) {
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index 1efe35c206b..a3fdf36cbd5 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -114,14 +114,12 @@ def _get_party_details(
set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype)
)
party = party_details[party_type.lower()]
-
- if not ignore_permissions and not (
- frappe.has_permission(party_type, "read", party)
- or frappe.has_permission(party_type, "select", party)
- ):
- frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError)
-
party = frappe.get_doc(party_type, party)
+
+ if not ignore_permissions:
+ ptype = "select" if frappe.only_has_select_perm(party_type) else "read"
+ frappe.has_permission(party_type, ptype, party, throw=True)
+
currency = party.get("default_currency") or currency or get_company_currency(company)
party_address, shipping_address = set_address_details(
@@ -637,9 +635,7 @@ def get_due_date_from_template(template_name, posting_date, bill_date):
return due_date
-def validate_due_date(
- posting_date, due_date, party_type, party, company=None, bill_date=None, template_name=None
-):
+def validate_due_date(posting_date, due_date, bill_date=None, template_name=None):
if getdate(due_date) < getdate(posting_date):
frappe.throw(_("Due Date cannot be before Posting / Supplier Invoice Date"))
else:
diff --git a/erpnext/accounts/report/financial_ratios/financial_ratios.py b/erpnext/accounts/report/financial_ratios/financial_ratios.py
index 57421ebcb01..47b4fd0da08 100644
--- a/erpnext/accounts/report/financial_ratios/financial_ratios.py
+++ b/erpnext/accounts/report/financial_ratios/financial_ratios.py
@@ -177,8 +177,8 @@ def add_solvency_ratios(
return_on_equity_ratio = {"ratio": "Return on Equity Ratio"}
for year in years:
- profit_after_tax = total_income[year] + total_expense[year]
- share_holder_fund = total_asset[year] - total_liability[year]
+ profit_after_tax = flt(total_income.get(year)) + flt(total_expense.get(year))
+ share_holder_fund = flt(total_asset.get(year)) - flt(total_liability.get(year))
debt_equity_ratio[year] = calculate_ratio(
total_liability.get(year), share_holder_fund, precision
diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py
index 9f96449ba7c..0912c7270de 100644
--- a/erpnext/accounts/report/utils.py
+++ b/erpnext/accounts/report/utils.py
@@ -251,6 +251,7 @@ def get_journal_entries(filters, args):
)
.where(
(je.voucher_type == "Journal Entry")
+ & (je.docstatus == 1)
& (journal_account.party == filters.get(args.party))
& (journal_account.account.isin(args.party_account))
)
@@ -281,7 +282,9 @@ def get_payment_entries(filters, args):
pe.cost_center,
)
.where(
- (pe.party == filters.get(args.party)) & (pe[args.account_fieldname].isin(args.party_account))
+ (pe.docstatus == 1)
+ & (pe.party == filters.get(args.party))
+ & (pe[args.account_fieldname].isin(args.party_account))
)
.orderby(pe.posting_date, pe.name, order=Order.desc)
)
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index 58fd6d4ef8a..02e7a9bb292 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -571,10 +571,16 @@ frappe.ui.form.on('Asset', {
indicator: 'red'
});
}
- frm.set_value('gross_purchase_amount', item.base_net_rate + item.item_tax_amount);
- frm.set_value('purchase_receipt_amount', item.base_net_rate + item.item_tax_amount);
- item.asset_location && frm.set_value('location', item.asset_location);
+ var is_grouped_asset = frappe.db.get_value('Item', item.item_code, 'is_grouped_asset');
+ var asset_quantity = is_grouped_asset ? item.qty : 1;
+ var purchase_amount = flt(item.valuation_rate * asset_quantity, precision('gross_purchase_amount'));
+
+ frm.set_value('gross_purchase_amount', purchase_amount);
+ frm.set_value('purchase_receipt_amount', purchase_amount);
+ frm.set_value('asset_quantity', asset_quantity);
frm.set_value('cost_center', item.cost_center || purchase_doc.cost_center);
+ if(item.asset_location) { frm.set_value('location', item.asset_location); }
+
},
set_depreciation_rate: function(frm, row) {
diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json
index ac712d44316..d0c9350d777 100644
--- a/erpnext/assets/doctype/asset/asset.json
+++ b/erpnext/assets/doctype/asset/asset.json
@@ -202,9 +202,9 @@
"fieldname": "purchase_date",
"fieldtype": "Date",
"label": "Purchase Date",
+ "mandatory_depends_on": "eval:!doc.is_existing_asset",
"read_only": 1,
- "read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset",
- "reqd": 1
+ "read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset"
},
{
"fieldname": "disposal_date",
@@ -227,15 +227,15 @@
"fieldname": "gross_purchase_amount",
"fieldtype": "Currency",
"label": "Gross Purchase Amount",
+ "mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)",
"options": "Company:company:default_currency",
- "read_only_depends_on": "eval:!doc.is_existing_asset",
- "reqd": 1
+ "read_only_depends_on": "eval:!doc.is_existing_asset"
},
{
"fieldname": "available_for_use_date",
"fieldtype": "Date",
"label": "Available-for-use Date",
- "reqd": 1
+ "mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)"
},
{
"default": "0",
@@ -590,7 +590,7 @@
"link_fieldname": "target_asset"
}
],
- "modified": "2023-12-21 16:46:20.732869",
+ "modified": "2024-01-05 17:36:53.131512",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 4b4579b461b..a7e6ae9afbd 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -57,7 +57,7 @@ class Asset(AccountsController):
asset_owner: DF.Literal["", "Company", "Supplier", "Customer"]
asset_owner_company: DF.Link | None
asset_quantity: DF.Int
- available_for_use_date: DF.Date
+ available_for_use_date: DF.Date | None
booked_fixed_asset: DF.Check
calculate_depreciation: DF.Check
capitalized_in: DF.Link | None
@@ -92,7 +92,7 @@ class Asset(AccountsController):
number_of_depreciations_booked: DF.Int
opening_accumulated_depreciation: DF.Currency
policy_number: DF.Data | None
- purchase_date: DF.Date
+ purchase_date: DF.Date | None
purchase_invoice: DF.Link | None
purchase_receipt: DF.Link | None
purchase_receipt_amount: DF.Currency
@@ -316,7 +316,12 @@ class Asset(AccountsController):
frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError)
if is_cwip_accounting_enabled(self.asset_category):
- if not self.is_existing_asset and not (self.purchase_receipt or self.purchase_invoice):
+ if (
+ not self.is_existing_asset
+ and not self.is_composite_asset
+ and not self.purchase_receipt
+ and not self.purchase_invoice
+ ):
frappe.throw(
_("Please create purchase receipt or purchase invoice for the item {0}").format(
self.item_code
@@ -329,7 +334,7 @@ class Asset(AccountsController):
and not frappe.db.get_value("Purchase Invoice", self.purchase_invoice, "update_stock")
):
frappe.throw(
- _("Update stock must be enable for the purchase invoice {0}").format(self.purchase_invoice)
+ _("Update stock must be enabled for the purchase invoice {0}").format(self.purchase_invoice)
)
if not self.calculate_depreciation:
diff --git a/erpnext/assets/doctype/asset_activity/asset_activity.py b/erpnext/assets/doctype/asset_activity/asset_activity.py
index a64cb1aba3d..7177223b4f6 100644
--- a/erpnext/assets/doctype/asset_activity/asset_activity.py
+++ b/erpnext/assets/doctype/asset_activity/asset_activity.py
@@ -3,6 +3,7 @@
import frappe
from frappe.model.document import Document
+from frappe.utils import now_datetime
class AssetActivity(Document):
@@ -30,5 +31,6 @@ def add_asset_activity(asset, subject):
"asset": asset,
"subject": subject,
"user": frappe.session.user,
+ "date": now_datetime(),
}
).insert(ignore_permissions=True, ignore_links=True)
diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py
index 034ec555dcd..d401b81c2ed 100644
--- a/erpnext/assets/doctype/asset_category/asset_category.py
+++ b/erpnext/assets/doctype/asset_category/asset_category.py
@@ -86,12 +86,12 @@ class AssetCategory(Document):
if selected_key_type not in expected_key_types:
frappe.throw(
_(
- "Row #{}: {} of {} should be {}. Please modify the account or select a different account."
+ "Row #{0}: {1} of {2} should be {3}. Please update the {1} or select a different account."
).format(
d.idx,
frappe.unscrub(key_to_match),
frappe.bold(selected_account),
- frappe.bold(expected_key_types),
+ frappe.bold(" or ".join(expected_key_types)),
),
title=_("Invalid Account"),
)
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json
index be35914251d..73838163d3a 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json
@@ -9,6 +9,7 @@
"field_order": [
"asset",
"naming_series",
+ "company",
"column_break_2",
"gross_purchase_amount",
"opening_accumulated_depreciation",
@@ -193,12 +194,20 @@
"fieldtype": "Check",
"label": "Depreciate based on shifts",
"read_only": 1
+ },
+ {
+ "fetch_from": "asset.company",
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-11-29 00:57:00.461998",
+ "modified": "2024-01-08 16:31:04.533928",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Depreciation Schedule",
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
index 67234ccd843..4c94be53203 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
@@ -35,6 +35,7 @@ class AssetDepreciationSchedule(Document):
amended_from: DF.Link | None
asset: DF.Link
+ company: DF.Link | None
daily_prorata_based: DF.Check
depreciation_method: DF.Literal[
"", "Straight Line", "Double Declining Balance", "Written Down Value", "Manual"
diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json
index 3f8559e63f0..b05de7d0b2e 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.json
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -214,7 +214,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2023-11-28 13:01:18.403492",
+ "modified": "2024-01-05 15:26:02.320942",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",
@@ -238,6 +238,41 @@
"role": "Purchase Manager",
"share": 1,
"write": 1
+ },
+ {
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "Accounts User",
+ "share": 1
+ },
+ {
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "Accounts Manager",
+ "share": 1
+ },
+ {
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "Stock Manager",
+ "share": 1
+ },
+ {
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "Stock User",
+ "share": 1
+ },
+ {
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "Purchase User",
+ "share": 1
}
],
"sort_field": "modified",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 4c274354b21..a28a310306f 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -452,6 +452,7 @@ class PurchaseOrder(BuyingController):
self.update_requested_qty()
self.update_ordered_qty()
self.update_reserved_qty_for_subcontract()
+ self.update_subcontracting_order_status()
self.notify_update()
clear_doctype_notifications(self)
@@ -613,6 +614,17 @@ class PurchaseOrder(BuyingController):
if frappe.db.get_single_value("Buying Settings", "auto_create_subcontracting_order"):
make_subcontracting_order(self.name, save=True, notify=True)
+ def update_subcontracting_order_status(self):
+ from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
+ update_subcontracting_order_status as update_sco_status,
+ )
+
+ if self.is_subcontracted and not self.is_old_subcontracting_flow:
+ sco = frappe.db.get_value("Subcontracting Order", {"purchase_order": self.name, "docstatus": 1})
+
+ if sco:
+ update_sco_status(sco, "Closed" if self.status == "Closed" else None)
+
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/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 9c3135d6b10..7986c3d4809 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -129,6 +129,17 @@ class AccountsController(TransactionBase):
if self.doctype in relevant_docs:
self.set_payment_schedule()
+ def remove_bundle_for_non_stock_invoices(self):
+ has_sabb = False
+ if self.doctype in ("Sales Invoice", "Purchase Invoice") and not self.update_stock:
+ for item in self.get("items"):
+ if item.serial_and_batch_bundle:
+ item.serial_and_batch_bundle = None
+ has_sabb = True
+
+ if has_sabb:
+ self.remove_serial_and_batch_bundle()
+
def ensure_supplier_is_not_blocked(self):
is_supplier_payment = self.doctype == "Payment Entry" and self.party_type == "Supplier"
is_buying_invoice = self.doctype in ["Purchase Invoice", "Purchase Order"]
@@ -156,6 +167,9 @@ class AccountsController(TransactionBase):
if self.get("_action") and self._action != "update_after_submit":
self.set_missing_values(for_validate=True)
+ if self.get("_action") == "submit":
+ self.remove_bundle_for_non_stock_invoices()
+
self.ensure_supplier_is_not_blocked()
self.validate_date_with_fiscal_year()
@@ -561,18 +575,12 @@ class AccountsController(TransactionBase):
validate_due_date(
self.posting_date,
self.due_date,
- "Customer",
- self.customer,
- self.company,
self.payment_terms_template,
)
elif self.doctype == "Purchase Invoice":
validate_due_date(
self.bill_date or self.posting_date,
self.due_date,
- "Supplier",
- self.supplier,
- self.company,
self.bill_date,
self.payment_terms_template,
)
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 572fa519e1e..fb680100b7d 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -744,11 +744,8 @@ class BuyingController(SubcontractingController):
item_data = frappe.db.get_value(
"Item", row.item_code, ["asset_naming_series", "asset_category"], as_dict=1
)
-
- if is_grouped_asset:
- purchase_amount = flt(row.base_amount + row.item_tax_amount)
- else:
- purchase_amount = flt(row.base_rate + row.item_tax_amount)
+ asset_quantity = row.qty if is_grouped_asset else 1
+ purchase_amount = flt(row.valuation_rate) * asset_quantity
asset = frappe.get_doc(
{
@@ -764,7 +761,7 @@ class BuyingController(SubcontractingController):
"calculate_depreciation": 0,
"purchase_receipt_amount": purchase_amount,
"gross_purchase_amount": purchase_amount,
- "asset_quantity": row.qty if is_grouped_asset else 1,
+ "asset_quantity": asset_quantity,
"purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None,
"purchase_invoice": self.name if self.doctype == "Purchase Invoice" else None,
}
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index e7bd2a7265c..6e50279d040 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -10,7 +10,7 @@ from frappe.utils import flt, format_datetime, get_datetime
import erpnext
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle
-from erpnext.stock.utils import get_incoming_rate
+from erpnext.stock.utils import get_incoming_rate, get_valuation_method
class StockOverReturnError(frappe.ValidationError):
@@ -116,7 +116,12 @@ def validate_returned_items(doc):
ref = valid_items.get(d.item_code, frappe._dict())
validate_quantity(doc, d, ref, valid_items, already_returned_items)
- if ref.rate and doc.doctype in ("Delivery Note", "Sales Invoice") and flt(d.rate) > ref.rate:
+ if (
+ ref.rate
+ and flt(d.rate) > ref.rate
+ and doc.doctype in ("Delivery Note", "Sales Invoice")
+ and get_valuation_method(ref.item_code) != "Moving Average"
+ ):
frappe.throw(
_("Row # {0}: Rate cannot be greater than the rate used in {1} {2}").format(
d.idx, doc.doctype, doc.return_against
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 919e459c9e2..22b0d08c92a 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -432,6 +432,9 @@ class SellingController(StockController):
items = self.get("items") + (self.get("packed_items") or [])
for d in items:
+ if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"):
+ continue
+
if not self.get("return_against") or (
get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")
):
diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py
index d09001c8fc1..297f8c26be9 100644
--- a/erpnext/controllers/status_updater.py
+++ b/erpnext/controllers/status_updater.py
@@ -131,11 +131,6 @@ status_map = {
"eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Manufacture'",
],
],
- "Bank Transaction": [
- ["Unreconciled", "eval:self.docstatus == 1 and self.unallocated_amount>0"],
- ["Reconciled", "eval:self.docstatus == 1 and self.unallocated_amount<=0"],
- ["Cancelled", "eval:self.docstatus == 2"],
- ],
"POS Opening Entry": [
["Draft", None],
["Open", "eval:self.docstatus == 1 and not self.pos_closing_entry"],
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 78bb2d2c270..d5cb6f5b981 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -494,6 +494,7 @@ bank_reconciliation_doctypes = [
"Payment Entry",
"Journal Entry",
"Purchase Invoice",
+ "Sales Invoice",
]
accounting_dimension_doctypes = [
diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
index 6100756a6a2..ceb4406170a 100644
--- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
+++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
@@ -117,7 +117,7 @@ class MaintenanceSchedule(TransactionBase):
self.update_amc_date(serial_nos, d.end_date)
no_email_sp = []
- if d.sales_person not in email_map:
+ if d.sales_person and d.sales_person not in email_map:
sp = frappe.get_doc("Sales Person", d.sales_person)
try:
email_map[d.sales_person] = sp.get_email_id()
@@ -131,12 +131,11 @@ class MaintenanceSchedule(TransactionBase):
).format(self.owner, "
" + "
".join(no_email_sp))
)
- scheduled_date = frappe.db.sql(
- """select scheduled_date from
- `tabMaintenance Schedule Detail` where sales_person=%s and item_code=%s and
- parent=%s""",
- (d.sales_person, d.item_code, self.name),
- as_dict=1,
+ scheduled_date = frappe.db.get_all(
+ "Maintenance Schedule Detail",
+ {"parent": self.name, "item_code": d.item_code},
+ ["scheduled_date"],
+ as_list=False,
)
for key in scheduled_date:
@@ -232,8 +231,6 @@ class MaintenanceSchedule(TransactionBase):
throw(_("Please select Start Date and End Date for Item {0}").format(d.item_code))
elif not d.no_of_visits:
throw(_("Please mention no of visits required"))
- elif not d.sales_person:
- throw(_("Please select a Sales Person for item: {0}").format(d.item_name))
if getdate(d.start_date) >= getdate(d.end_date):
throw(_("Start date should be less than end date for Item {0}").format(d.item_code))
@@ -452,20 +449,28 @@ def get_serial_nos_from_schedule(item_code, schedule=None):
def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=None):
from frappe.model.mapper import get_mapped_doc
+ def condition(doc):
+ if s_id:
+ return doc.name == s_id
+ elif item_name:
+ return doc.item_name == item_name
+
+ return True
+
def update_status_and_detail(source, target, parent):
target.maintenance_type = "Scheduled"
- target.maintenance_schedule_detail = s_id
def update_serial(source, target, parent):
- if source.serial_and_batch_bundle:
- serial_nos = frappe.get_doc(
- "Serial and Batch Bundle", source.serial_and_batch_bundle
- ).get_serial_nos()
+ if source.item_reference:
+ if sbb := frappe.db.get_value(
+ "Maintenance Schedule Item", source.item_reference, "serial_and_batch_bundle"
+ ):
+ serial_nos = frappe.get_doc("Serial and Batch Bundle", sbb).get_serial_nos()
- if len(serial_nos) == 1:
- target.serial_no = serial_nos[0]
- else:
- target.serial_no = ""
+ if len(serial_nos) == 1:
+ target.serial_no = serial_nos[0]
+ else:
+ target.serial_no = ""
doclist = get_mapped_doc(
"Maintenance Schedule",
@@ -477,10 +482,13 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No
"validation": {"docstatus": ["=", 1]},
"postprocess": update_status_and_detail,
},
- "Maintenance Schedule Item": {
+ "Maintenance Schedule Detail": {
"doctype": "Maintenance Visit Purpose",
- "condition": lambda doc: doc.item_name == item_name if item_name else True,
- "field_map": {"sales_person": "service_person"},
+ "condition": condition,
+ "field_map": {
+ "sales_person": "service_person",
+ "name": "maintenance_schedule_detail",
+ },
"postprocess": update_serial,
},
},
diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py
index e7df4847ddb..d2511b8cbc0 100644
--- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py
+++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py
@@ -56,20 +56,39 @@ class MaintenanceVisit(TransactionBase):
frappe.throw(_("Add Items in the Purpose Table"), title=_("Purposes Required"))
def validate_maintenance_date(self):
- if self.maintenance_type == "Scheduled" and self.maintenance_schedule_detail:
- item_ref = frappe.db.get_value(
- "Maintenance Schedule Detail", self.maintenance_schedule_detail, "item_reference"
- )
- if item_ref:
- start_date, end_date = frappe.db.get_value(
- "Maintenance Schedule Item", item_ref, ["start_date", "end_date"]
+ if self.maintenance_type == "Scheduled":
+ if self.maintenance_schedule_detail:
+ item_ref = frappe.db.get_value(
+ "Maintenance Schedule Detail", self.maintenance_schedule_detail, "item_reference"
)
- if get_datetime(self.mntc_date) < get_datetime(start_date) or get_datetime(
- self.mntc_date
- ) > get_datetime(end_date):
- frappe.throw(
- _("Date must be between {0} and {1}").format(format_date(start_date), format_date(end_date))
+ if item_ref:
+ start_date, end_date = frappe.db.get_value(
+ "Maintenance Schedule Item", item_ref, ["start_date", "end_date"]
)
+ if get_datetime(self.mntc_date) < get_datetime(start_date) or get_datetime(
+ self.mntc_date
+ ) > get_datetime(end_date):
+ frappe.throw(
+ _("Date must be between {0} and {1}").format(format_date(start_date), format_date(end_date))
+ )
+ else:
+ for purpose in self.purposes:
+ if purpose.maintenance_schedule_detail:
+ item_ref = frappe.db.get_value(
+ "Maintenance Schedule Detail", purpose.maintenance_schedule_detail, "item_reference"
+ )
+ if item_ref:
+ start_date, end_date = frappe.db.get_value(
+ "Maintenance Schedule Item", item_ref, ["start_date", "end_date"]
+ )
+ if get_datetime(self.mntc_date) < get_datetime(start_date) or get_datetime(
+ self.mntc_date
+ ) > get_datetime(end_date):
+ frappe.throw(
+ _("Date must be between {0} and {1}").format(
+ format_date(start_date), format_date(end_date)
+ )
+ )
def validate(self):
self.validate_serial_no()
@@ -82,6 +101,7 @@ class MaintenanceVisit(TransactionBase):
if not cancel:
status = self.completion_status
actual_date = self.mntc_date
+
if self.maintenance_schedule_detail:
frappe.db.set_value(
"Maintenance Schedule Detail", self.maintenance_schedule_detail, "completion_status", status
@@ -89,6 +109,21 @@ class MaintenanceVisit(TransactionBase):
frappe.db.set_value(
"Maintenance Schedule Detail", self.maintenance_schedule_detail, "actual_date", actual_date
)
+ else:
+ for purpose in self.purposes:
+ if purpose.maintenance_schedule_detail:
+ frappe.db.set_value(
+ "Maintenance Schedule Detail",
+ purpose.maintenance_schedule_detail,
+ "completion_status",
+ status,
+ )
+ frappe.db.set_value(
+ "Maintenance Schedule Detail",
+ purpose.maintenance_schedule_detail,
+ "actual_date",
+ actual_date,
+ )
def update_customer_issue(self, flag):
if not self.maintenance_schedule:
diff --git a/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json b/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json
index ba053555531..a5a63c4c4de 100644
--- a/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json
+++ b/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json
@@ -17,7 +17,8 @@
"work_details",
"work_done",
"prevdoc_doctype",
- "prevdoc_docname"
+ "prevdoc_docname",
+ "maintenance_schedule_detail"
],
"fields": [
{
@@ -49,6 +50,8 @@
"options": "Serial No"
},
{
+ "fetch_from": "item_code.description",
+ "fetch_if_empty": 1,
"fieldname": "description",
"fieldtype": "Text Editor",
"in_list_view": 1,
@@ -56,7 +59,6 @@
"oldfieldname": "description",
"oldfieldtype": "Small Text",
"print_width": "300px",
- "reqd": 1,
"width": "300px"
},
{
@@ -103,12 +105,19 @@
{
"fieldname": "section_break_6",
"fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "maintenance_schedule_detail",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Maintenance Schedule Detail",
+ "options": "Maintenance Schedule Detail"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2023-02-27 11:09:33.114458",
+ "modified": "2024-01-05 21:46:53.239830",
"modified_by": "Administrator",
"module": "Maintenance",
"name": "Maintenance Visit Purpose",
diff --git a/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.py b/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.py
index 3686941c640..1d4dab28738 100644
--- a/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.py
+++ b/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.py
@@ -14,9 +14,10 @@ class MaintenanceVisitPurpose(Document):
if TYPE_CHECKING:
from frappe.types import DF
- description: DF.TextEditor
+ description: DF.TextEditor | None
item_code: DF.Link | None
item_name: DF.Data | None
+ maintenance_schedule_detail: DF.Data | None
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
diff --git a/erpnext/manufacturing/dashboard_chart/completed_operation/completed_operation.json b/erpnext/manufacturing/dashboard_chart/completed_operation/completed_operation.json
index d74ae2faf4d..146214af429 100644
--- a/erpnext/manufacturing/dashboard_chart/completed_operation/completed_operation.json
+++ b/erpnext/manufacturing/dashboard_chart/completed_operation/completed_operation.json
@@ -12,12 +12,13 @@
"is_public": 1,
"is_standard": 1,
"last_synced_on": "2020-07-21 16:57:09.767009",
- "modified": "2020-07-21 16:57:55.719802",
+ "modified": "2024-01-10 12:21:25.134075",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Completed Operation",
"number_of_groups": 0,
"owner": "Administrator",
+ "parent_document_type": "Work Order",
"time_interval": "Quarterly",
"timeseries": 1,
"timespan": "Last Year",
diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py
index a2919b79b80..f013b88e946 100644
--- a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py
+++ b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py
@@ -86,10 +86,12 @@ def get_ancestor_boms(new_bom: str, bom_list: Optional[List] = None) -> List:
if new_bom == d.parent:
frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(new_bom, d.parent))
- bom_list.append(d.parent)
+ if d.parent not in tuple(bom_list):
+ bom_list.append(d.parent)
+
get_ancestor_boms(d.parent, bom_list)
- return list(set(bom_list))
+ return bom_list
def update_new_bom_in_bom_items(unit_cost: float, current_bom: str, new_bom: str) -> None:
diff --git a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py
index b38fc8976b2..30e6f5e2091 100644
--- a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py
+++ b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py
@@ -57,6 +57,68 @@ class TestBOMUpdateLog(FrappeTestCase):
log.reload()
self.assertEqual(log.status, "Completed")
+ def test_bom_replace_for_root_bom(self):
+ """
+ - B-Item A (Root Item)
+ - B-Item B
+ - B-Item C
+ - B-Item D
+ - B-Item E
+ - B-Item F
+
+ Create New BOM for B-Item E with B-Item G and replace it in the above BOM.
+ """
+
+ from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ items = ["B-Item A", "B-Item B", "B-Item C", "B-Item D", "B-Item E", "B-Item F", "B-Item G"]
+
+ for item_code in items:
+ if not frappe.db.exists("Item", item_code):
+ make_item(item_code)
+
+ for item_code in items:
+ remove_bom(item_code)
+
+ bom_tree = {
+ "B-Item A": {"B-Item B": {"B-Item C": {}}, "B-Item D": {"B-Item E": {"B-Item F": {}}}}
+ }
+
+ root_bom = create_nested_bom(bom_tree, prefix="")
+
+ exploded_items = frappe.get_all(
+ "BOM Explosion Item", filters={"parent": root_bom.name}, fields=["item_code"]
+ )
+
+ exploded_items = [item.item_code for item in exploded_items]
+ expected_exploded_items = ["B-Item C", "B-Item F"]
+ self.assertEqual(sorted(exploded_items), sorted(expected_exploded_items))
+
+ old_bom = frappe.db.get_value("BOM", {"item": "B-Item E"}, "name")
+ bom_tree = {"B-Item E": {"B-Item G": {}}}
+
+ new_bom = create_nested_bom(bom_tree, prefix="")
+ enqueue_replace_bom(boms=frappe._dict(current_bom=old_bom, new_bom=new_bom.name))
+
+ exploded_items = frappe.get_all(
+ "BOM Explosion Item", filters={"parent": root_bom.name}, fields=["item_code"]
+ )
+
+ exploded_items = [item.item_code for item in exploded_items]
+ expected_exploded_items = ["B-Item C", "B-Item G"]
+ self.assertEqual(sorted(exploded_items), sorted(expected_exploded_items))
+
+
+def remove_bom(item_code):
+ boms = frappe.get_all("BOM", fields=["docstatus", "name"], filters={"item": item_code})
+
+ for row in boms:
+ if row.docstatus == 1:
+ frappe.get_doc("BOM", row.name).cancel()
+
+ frappe.delete_doc("BOM", row.name)
+
def update_cost_in_all_boms_in_test():
"""
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index caa6e464d29..3943b13b827 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -646,6 +646,10 @@ class ProductionPlan(Document):
"project": self.project,
}
+ key = (d.item_code, d.sales_order, d.warehouse)
+ if not d.sales_order:
+ key = (d.name, d.item_code, d.warehouse)
+
if not item_details["project"] and d.sales_order:
item_details["project"] = frappe.get_cached_value("Sales Order", d.sales_order, "project")
@@ -654,12 +658,9 @@ class ProductionPlan(Document):
item_dict[(d.item_code, d.material_request_item, d.warehouse)] = item_details
else:
item_details.update(
- {
- "qty": flt(item_dict.get((d.item_code, d.sales_order, d.warehouse), {}).get("qty"))
- + (flt(d.planned_qty) - flt(d.ordered_qty))
- }
+ {"qty": flt(item_dict.get(key, {}).get("qty")) + (flt(d.planned_qty) - flt(d.ordered_qty))}
)
- item_dict[(d.item_code, d.sales_order, d.warehouse)] = item_details
+ item_dict[key] = item_details
return item_dict
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index f6dfaa50586..fedeb7a4777 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -672,7 +672,7 @@ class TestProductionPlan(FrappeTestCase):
items_data = pln.get_production_items()
# Update qty
- items_data[(item, None, None)]["qty"] = qty
+ items_data[(pln.po_items[0].name, item, None)]["qty"] = qty
# Create and Submit Work Order for each item in items_data
for key, item in items_data.items():
@@ -1522,6 +1522,45 @@ class TestProductionPlan(FrappeTestCase):
for d in mr_items:
self.assertEqual(d.get("quantity"), 1000.0)
+ def test_fg_item_quantity(self):
+ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+ from erpnext.stock.utils import get_or_make_bin
+
+ fg_item = make_item(properties={"is_stock_item": 1}).name
+ rm_item = make_item(properties={"is_stock_item": 1}).name
+
+ make_bom(item=fg_item, raw_materials=[rm_item], source_warehouse="_Test Warehouse - _TC")
+
+ pln = create_production_plan(item_code=fg_item, planned_qty=10, do_not_submit=1)
+
+ pln.append(
+ "po_items",
+ {
+ "item_code": rm_item,
+ "planned_qty": 20,
+ "bom_no": pln.po_items[0].bom_no,
+ "warehouse": pln.po_items[0].warehouse,
+ "planned_start_date": add_to_date(nowdate(), days=1),
+ },
+ )
+ pln.submit()
+ wo_qty = {}
+
+ for row in pln.po_items:
+ wo_qty[row.name] = row.planned_qty
+
+ pln.make_work_order()
+
+ work_orders = frappe.get_all(
+ "Work Order",
+ fields=["qty", "production_plan_item as name"],
+ filters={"production_plan": pln.name},
+ )
+ self.assertEqual(len(work_orders), 2)
+
+ for row in work_orders:
+ self.assertEqual(row.qty, wo_qty[row.name])
+
def create_production_plan(**args):
"""
diff --git a/erpnext/patches/v14_0/update_invoicing_period_in_subscription.py b/erpnext/patches/v14_0/update_invoicing_period_in_subscription.py
index 2879e57e1a2..b70548ccb7f 100644
--- a/erpnext/patches/v14_0/update_invoicing_period_in_subscription.py
+++ b/erpnext/patches/v14_0/update_invoicing_period_in_subscription.py
@@ -4,5 +4,5 @@ import frappe
def execute():
subscription = frappe.qb.DocType("Subscription")
frappe.qb.update(subscription).set(
- subscription.generate_invoice_at, "Beginning of the currency subscription period"
+ subscription.generate_invoice_at, "Beginning of the current subscription period"
).where(subscription.generate_invoice_at_period_start == 1).run()
diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json
index 715b09c64bc..5917e9b5d26 100644
--- a/erpnext/projects/doctype/project/project.json
+++ b/erpnext/projects/doctype/project/project.json
@@ -131,6 +131,7 @@
"set_only_once": 1
},
{
+ "bold": 1,
"fieldname": "expected_start_date",
"fieldtype": "Date",
"label": "Expected Start Date",
@@ -453,7 +454,7 @@
"index_web_pages_for_search": 1,
"links": [],
"max_attachments": 4,
- "modified": "2023-08-28 22:27:28.370849",
+ "modified": "2024-01-08 16:01:34.598258",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project",
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index 4f2e39539d5..656550aaafc 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -19,6 +19,62 @@ from erpnext.setup.doctype.holiday_list.holiday_list import is_holiday
class Project(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
+
+ from erpnext.projects.doctype.project_user.project_user import ProjectUser
+
+ actual_end_date: DF.Date | None
+ actual_start_date: DF.Date | None
+ actual_time: DF.Float
+ collect_progress: DF.Check
+ company: DF.Link
+ copied_from: DF.Data | None
+ cost_center: DF.Link | None
+ customer: DF.Link | None
+ daily_time_to_send: DF.Time | None
+ day_to_send: DF.Literal[
+ "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"
+ ]
+ department: DF.Link | None
+ estimated_costing: DF.Currency
+ expected_end_date: DF.Date | None
+ expected_start_date: DF.Date | None
+ first_email: DF.Time | None
+ frequency: DF.Literal["Hourly", "Twice Daily", "Daily", "Weekly"]
+ from_time: DF.Time | None
+ gross_margin: DF.Currency
+ holiday_list: DF.Link | None
+ is_active: DF.Literal["Yes", "No"]
+ message: DF.Text | None
+ naming_series: DF.Literal["PROJ-.####"]
+ notes: DF.TextEditor | None
+ per_gross_margin: DF.Percent
+ percent_complete: DF.Percent
+ percent_complete_method: DF.Literal["Manual", "Task Completion", "Task Progress", "Task Weight"]
+ priority: DF.Literal["Medium", "Low", "High"]
+ project_name: DF.Data
+ project_template: DF.Link | None
+ project_type: DF.Link | None
+ sales_order: DF.Link | None
+ second_email: DF.Time | None
+ status: DF.Literal["Open", "Completed", "Cancelled"]
+ to_time: DF.Time | None
+ total_billable_amount: DF.Currency
+ total_billed_amount: DF.Currency
+ total_consumed_material_cost: DF.Currency
+ total_costing_amount: DF.Currency
+ total_purchase_cost: DF.Currency
+ total_sales_amount: DF.Currency
+ users: DF.Table[ProjectUser]
+ weekly_time_to_send: DF.Time | None
+ # end: auto-generated types
+
def onload(self):
self.set_onload(
"activity_summary",
@@ -314,18 +370,16 @@ def get_timeline_data(doctype: str, name: str) -> dict[int, int]:
def get_project_list(
doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified"
):
- user = frappe.session.user
customers, suppliers = get_customers_suppliers("Project", frappe.session.user)
ignore_permissions = False
- if is_website_user():
+ if is_website_user() and frappe.session.user != "Guest":
if not filters:
filters = []
if customers:
filters.append([doctype, "customer", "in", customers])
-
- ignore_permissions = True
+ ignore_permissions = True
meta = frappe.get_meta(doctype)
diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json
index 4d2d2252423..cc9832b5845 100644
--- a/erpnext/projects/doctype/task/task.json
+++ b/erpnext/projects/doctype/task/task.json
@@ -153,6 +153,7 @@
"label": "Timeline"
},
{
+ "bold": 1,
"fieldname": "exp_start_date",
"fieldtype": "Date",
"label": "Expected Start Date",
@@ -398,7 +399,7 @@
"is_tree": 1,
"links": [],
"max_attachments": 5,
- "modified": "2023-11-20 11:42:41.884069",
+ "modified": "2024-01-08 16:00:41.296203",
"modified_by": "Administrator",
"module": "Projects",
"name": "Task",
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 07b1e8f8477..36cc66313de 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -454,7 +454,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
item.weight_uom = '';
item.conversion_factor = 0;
- if(['Sales Invoice'].includes(this.frm.doc.doctype)) {
+ if(['Sales Invoice', 'Purchase Invoice'].includes(this.frm.doc.doctype)) {
update_stock = cint(me.frm.doc.update_stock);
show_batch_dialog = update_stock;
@@ -545,7 +545,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
},
() => me.toggle_conversion_factor(item),
() => {
- if (show_batch_dialog)
+ if (show_batch_dialog && !frappe.flags.trigger_from_barcode_scanner)
return frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
.then((r) => {
if (r.message &&
@@ -790,7 +790,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if (me.frm.doc.price_list_currency == company_currency) {
me.frm.set_value('plc_conversion_rate', 1.0);
}
- if (company_doc.default_letter_head) {
+ if (company_doc && company_doc.default_letter_head) {
if(me.frm.fields_dict.letter_head) {
me.frm.set_value("letter_head", company_doc.default_letter_head);
}
@@ -1239,6 +1239,20 @@ 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];
@@ -1576,6 +1590,18 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
return item_list;
}
+ items_delete() {
+ this.update_localstorage_scanned_data();
+ }
+
+ update_localstorage_scanned_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.update_localstorage_scanned_data();
+ }
+ }
+
_set_values_for_item_list(children) {
const items_rule_dict = {};
diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js
index a1ebfe9aa4a..cf7fab89ffb 100644
--- a/erpnext/public/js/utils/barcode_scanner.js
+++ b/erpnext/public/js/utils/barcode_scanner.js
@@ -7,8 +7,6 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
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
@@ -84,6 +82,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
update_table(data) {
return new Promise((resolve, reject) => {
let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
+ frappe.flags.trigger_from_barcode_scanner = true;
const {item_code, barcode, batch_no, serial_no, uom} = data;
@@ -106,50 +105,38 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
this.frm.has_items = false;
}
- if (this.is_duplicate_serial_no(row, serial_no)) {
+ if (serial_no && this.is_duplicate_serial_no(row, item_code, serial_no)) {
this.clean_up();
reject();
return;
}
frappe.run_serially([
- () => this.set_selector_trigger_flag(data),
- () => this.set_serial_no(row, serial_no),
- () => this.set_batch_no(row, batch_no),
+ () => this.set_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(),
- () => this.revert_selector_flag(),
- () => resolve(row)
+ () => resolve(row),
+ () => {
+ if (row.serial_and_batch_bundle && !this.frm.is_new()) {
+ this.frm.save();
+ }
+
+ frappe.flags.trigger_from_barcode_scanner = false;
+ }
]);
});
}
- // 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 {has_batch_no, has_serial_no} = data;
-
- const require_selecting_batch = has_batch_no;
- const require_selecting_serial = has_serial_no;
-
- if (!(require_selecting_batch || require_selecting_serial)) {
- frappe.flags.hide_serial_batch_dialog = true;
- }
- }
-
- revert_selector_flag() {
- frappe.flags.hide_serial_batch_dialog = 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);
+ frappe.flags.trigger_from_barcode_scanner = true;
await frappe.model.set_value(row.doctype, row.name, item_data);
return value;
};
@@ -158,8 +145,6 @@ 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,9 +167,8 @@ 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_no(row, this.dialog.get_value("serial_no")),
+ () => this.set_serial_and_batch(row, item_code, this.dialog.get_value("serial_no"), this.dialog.get_value("batch_no")),
() => this.add_child_for_remaining_qty(row),
() => this.clean_up()
]);
@@ -338,32 +322,144 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
}
}
- 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 = "";
-
- if (!!existing_serial_nos) {
- new_serial_nos = existing_serial_nos + "\n" + serial_no;
- } else {
- new_serial_nos = serial_no;
- }
- await frappe.model.set_value(row.doctype, row.name, this.serial_no_field, new_serial_nos);
+ 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,
+ },
+ })
}
}
+ 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;
+ }
+
+ for (let item_code in items) {
+ if (!existing_items.includes(item_code)) {
+ delete items[item_code];
+ }
+ }
+
+ localStorage[docname] = JSON.stringify(items);
+ }
+ }
+
+ 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);
@@ -379,13 +475,52 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
}
}
- is_duplicate_serial_no(row, serial_no) {
- const is_duplicate = row[this.serial_no_field]?.includes(serial_no);
+ is_duplicate_serial_no(row, item_code, serial_no) {
+ if (this.frm.is_new() || !row.serial_and_batch_bundle) {
+ let 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");
+ }
- if (is_duplicate) {
- this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
+ return 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");
+ }
+
+ return r.message;
+ })
}
- return is_duplicate;
+ }
+
+ async 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;
+ }
+
+ let existing_row = [];
+ if (entries[item_code]) {
+ existing_row = entries[item_code].filter((e) => e.serial_no === serial_no);
+ }
+
+ return existing_row.length;
}
get_row_to_modify_on_scan(item_code, batch_no, uom, barcode) {
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index 00b79e3aada..ab74f7f738b 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -370,15 +370,16 @@ def _make_sales_order(source_name, target_doc=None, customer_group=None, ignore_
)
# sales team
- for d in customer.get("sales_team") or []:
- target.append(
- "sales_team",
- {
- "sales_person": d.sales_person,
- "allocated_percentage": d.allocated_percentage or None,
- "commission_rate": d.commission_rate,
- },
- )
+ if not target.get("sales_team"):
+ for d in customer.get("sales_team") or []:
+ target.append(
+ "sales_team",
+ {
+ "sales_person": d.sales_person,
+ "allocated_percentage": d.allocated_percentage or None,
+ "commission_rate": d.commission_rate,
+ },
+ )
target.flags.ignore_permissions = ignore_permissions
target.delivery_date = nowdate()
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 09941eaa82c..95423612c85 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -582,17 +582,17 @@ class SalesOrder(SellingController):
def set_indicator(self):
"""Set indicator for portal"""
- if self.per_billed < 100 and self.per_delivered < 100:
- self.indicator_color = "orange"
- self.indicator_title = _("Not Paid and Not Delivered")
+ self.indicator_color = {
+ "Draft": "red",
+ "On Hold": "orange",
+ "To Deliver and Bill": "orange",
+ "To Bill": "orange",
+ "To Deliver": "orange",
+ "Completed": "green",
+ "Cancelled": "red",
+ }.get(self.status, "blue")
- elif self.per_billed == 100 and self.per_delivered < 100:
- self.indicator_color = "orange"
- self.indicator_title = _("Paid and Not Delivered")
-
- else:
- self.indicator_color = "green"
- self.indicator_title = _("Paid")
+ self.indicator_title = _(self.status)
def on_recurring(self, reference_doc, auto_repeat_doc):
def _get_delivery_date(ref_doc_delivery_date, red_doc_transaction_date, transaction_date):
diff --git a/erpnext/setup/demo.py b/erpnext/setup/demo.py
index 4bc98b91bd3..df2c49b2b62 100644
--- a/erpnext/setup/demo.py
+++ b/erpnext/setup/demo.py
@@ -149,6 +149,11 @@ def convert_order_to_invoices():
invoice.set_posting_time = 1
invoice.posting_date = order.transaction_date
invoice.due_date = order.transaction_date
+ invoice.bill_date = order.transaction_date
+
+ if invoice.get("payment_schedule"):
+ invoice.payment_schedule[0].due_date = order.transaction_date
+
invoice.update_stock = 1
invoice.submit()
diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json
index 1143ccb7b10..daf2df5a590 100644
--- a/erpnext/setup/doctype/employee/employee.json
+++ b/erpnext/setup/doctype/employee/employee.json
@@ -616,8 +616,8 @@
"fieldname": "relieving_date",
"fieldtype": "Date",
"label": "Relieving Date",
- "no_copy": 1,
"mandatory_depends_on": "eval:doc.status == \"Left\"",
+ "no_copy": 1,
"oldfieldname": "relieving_date",
"oldfieldtype": "Date"
},
@@ -822,12 +822,14 @@
"icon": "fa fa-user",
"idx": 24,
"image_field": "image",
+ "is_tree": 1,
"links": [],
- "modified": "2023-10-04 10:57:05.174592",
+ "modified": "2024-01-03 17:36:20.984421",
"modified_by": "Administrator",
"module": "Setup",
"name": "Employee",
"naming_rule": "By \"Naming Series\" field",
+ "nsm_parent_field": "reports_to",
"owner": "Administrator",
"permissions": [
{
@@ -860,7 +862,6 @@
"read": 1,
"report": 1,
"role": "HR Manager",
- "set_user_permissions": 1,
"share": 1,
"write": 1
}
@@ -871,4 +872,4 @@
"sort_order": "DESC",
"states": [],
"title_field": "employee_name"
-}
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py
index f71d21dd0b8..1c7018366af 100644
--- a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py
+++ b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py
@@ -65,7 +65,7 @@ class ClosingStockBalance(Document):
& (
(table.from_date.between(self.from_date, self.to_date))
| (table.to_date.between(self.from_date, self.to_date))
- | (table.from_date >= self.from_date and table.to_date <= self.to_date)
+ | (table.from_date >= self.from_date and table.to_date >= self.to_date)
)
)
)
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 3abd1d9e5ed..dae42895edb 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -1518,6 +1518,25 @@ class TestDeliveryNote(FrappeTestCase):
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 0
)
+ def test_internal_transfer_for_non_stock_item(self):
+ from erpnext.selling.doctype.customer.test_customer import create_internal_customer
+ from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
+
+ item = make_item(properties={"is_stock_item": 0}).name
+ warehouse = "_Test Warehouse - _TC"
+ target = "Stores - _TC"
+ company = "_Test Company"
+ customer = create_internal_customer(represents_company=company)
+ rate = 100
+
+ so = make_sales_order(item_code=item, qty=1, rate=rate, customer=customer, warehouse=warehouse)
+ dn = make_delivery_note(so.name)
+ dn.items[0].target_warehouse = target
+ dn.save().submit()
+
+ self.assertEqual(so.items[0].rate, rate)
+ self.assertEqual(dn.items[0].rate, so.items[0].rate)
+
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")
diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py
index 60624d4164b..d5eef5ad225 100644
--- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py
+++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py
@@ -305,7 +305,7 @@ def get_evaluated_inventory_dimension(doc, sl_dict, parent_doc=None):
dimensions = get_document_wise_inventory_dimensions(doc.doctype)
filter_dimensions = []
for row in dimensions:
- if row.type_of_transaction:
+ if row.type_of_transaction and row.type_of_transaction != "Both":
if (
row.type_of_transaction == "Inward"
if doc.docstatus == 1
diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py
index 33394e5a115..361c2f8cd98 100644
--- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py
+++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py
@@ -429,6 +429,14 @@ class TestInventoryDimension(FrappeTestCase):
)
warehouse = create_warehouse("Negative Stock Warehouse")
+
+ doc = make_stock_entry(item_code=item_code, source=warehouse, qty=10, do_not_submit=True)
+ doc.items[0].inv_site = "Site 1"
+ self.assertRaises(frappe.ValidationError, doc.submit)
+ doc.reload()
+ if doc.docstatus == 1:
+ doc.cancel()
+
doc = make_stock_entry(item_code=item_code, target=warehouse, qty=10, do_not_submit=True)
doc.items[0].to_inv_site = "Site 1"
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index d0b90c4565e..ec03be52ae1 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -202,6 +202,7 @@
"label": "Allow Alternative Item"
},
{
+ "allow_in_quick_entry": 1,
"bold": 1,
"default": "1",
"depends_on": "eval:!doc.is_fixed_asset",
@@ -239,6 +240,7 @@
"label": "Standard Selling Rate"
},
{
+ "allow_in_quick_entry": 1,
"default": "0",
"fieldname": "is_fixed_asset",
"fieldtype": "Check",
@@ -246,6 +248,7 @@
"set_only_once": 1
},
{
+ "allow_in_quick_entry": 1,
"depends_on": "is_fixed_asset",
"fieldname": "asset_category",
"fieldtype": "Link",
@@ -888,7 +891,7 @@
"index_web_pages_for_search": 1,
"links": [],
"make_attachments_public": 1,
- "modified": "2023-09-18 15:41:32.688051",
+ "modified": "2024-01-08 18:09:30.225085",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",
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 021c43e300c..c6518b45cd7 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
@@ -194,7 +194,8 @@ class LandedCostVoucher(Document):
for d in self.get("purchase_receipts"):
doc = frappe.get_doc(d.receipt_document_type, d.receipt_document)
# check if there are {qty} assets created and linked to this receipt document
- self.validate_asset_qty_and_status(d.receipt_document_type, doc)
+ if self.docstatus != 2:
+ self.validate_asset_qty_and_status(d.receipt_document_type, doc)
# set landed cost voucher amount in pr item
doc.set_landed_cost_voucher_amount()
@@ -235,20 +236,20 @@ class LandedCostVoucher(Document):
filters={receipt_document_type: item.receipt_document, "item_code": item.item_code},
fields=["name", "docstatus"],
)
- if not docs or len(docs) != item.qty:
+ if not docs or len(docs) < item.qty:
frappe.throw(
_(
- "There are not enough asset created or linked to {0}. Please create or link {1} Assets with respective document."
- ).format(item.receipt_document, item.qty)
+ "There are only {0} asset created or linked to {1}. Please create or link {2} Assets with respective document."
+ ).format(len(docs), item.receipt_document, item.qty)
)
if docs:
for d in docs:
if d.docstatus == 1:
frappe.throw(
_(
- "{2} {0} has submitted Assets. Remove Item {1} from table to continue."
+ "{0} {1} has submitted Assets. Remove Item {2} from table to continue."
).format(
- item.receipt_document, item.item_code, item.receipt_document_type
+ item.receipt_document_type, item.receipt_document, item.item_code
)
)
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index b7a64bb8b1a..517cc0342a8 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -717,7 +717,7 @@ class PurchaseReceipt(BuyingController):
):
warehouse_with_no_account.append(d.warehouse or d.rejected_warehouse)
- if d.is_fixed_asset:
+ if d.is_fixed_asset and d.landed_cost_voucher_amount:
self.update_assets(d, d.valuation_rate)
if warehouse_with_no_account:
@@ -849,11 +849,14 @@ class PurchaseReceipt(BuyingController):
)
for asset in assets:
+ purchase_amount = flt(valuation_rate) * asset.asset_quantity
frappe.db.set_value(
- "Asset", asset.name, "gross_purchase_amount", flt(valuation_rate) * asset.asset_quantity
- )
- frappe.db.set_value(
- "Asset", asset.name, "purchase_receipt_amount", flt(valuation_rate) * asset.asset_quantity
+ "Asset",
+ asset.name,
+ {
+ "gross_purchase_amount": purchase_amount,
+ "purchase_receipt_amount": purchase_amount,
+ },
)
def update_status(self, status):
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 218406f56fd..620b9606a71 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
@@ -729,19 +729,13 @@ class SerialandBatchBundle(Document):
def before_cancel(self):
self.delink_serial_and_batch_bundle()
- self.clear_table()
def delink_serial_and_batch_bundle(self):
- self.voucher_no = None
-
sles = frappe.get_all("Stock Ledger Entry", filters={"serial_and_batch_bundle": self.name})
for sle in sles:
frappe.db.set_value("Stock Ledger Entry", sle.name, "serial_and_batch_bundle", None)
- def clear_table(self):
- self.set("entries", [])
-
@property
def child_table(self):
if self.voucher_type == "Job Card":
@@ -876,7 +870,6 @@ class SerialandBatchBundle(Document):
self.validate_voucher_no_docstatus()
self.delink_refernce_from_voucher()
self.delink_reference_from_batch()
- self.clear_table()
@frappe.whitelist()
def add_serial_batch(self, data):
@@ -1011,13 +1004,17 @@ def make_serial_nos(item_code, serial_nos):
item = frappe.get_cached_value("Item", item_code, ["description", "item_code"], as_dict=1)
serial_nos = [d.get("serial_no") for d in serial_nos if d.get("serial_no")]
+ existing_serial_nos = frappe.get_all("Serial No", filters={"name": ("in", serial_nos)})
+
+ existing_serial_nos = [d.get("name") for d in existing_serial_nos if d.get("name")]
+ serial_nos = list(set(serial_nos) - set(existing_serial_nos))
+
+ if not serial_nos:
+ return
serial_nos_details = []
user = frappe.session.user
for serial_no in serial_nos:
- if frappe.db.exists("Serial No", serial_no):
- continue
-
serial_nos_details.append(
(
serial_no,
@@ -1053,9 +1050,16 @@ def make_serial_nos(item_code, serial_nos):
def make_batch_nos(item_code, batch_nos):
item = frappe.get_cached_value("Item", item_code, ["description", "item_code"], as_dict=1)
-
batch_nos = [d.get("batch_no") for d in batch_nos if d.get("batch_no")]
+ existing_batches = frappe.get_all("Batch", filters={"name": ("in", batch_nos)})
+
+ existing_batches = [d.get("name") for d in existing_batches if d.get("name")]
+
+ batch_nos = list(set(batch_nos) - set(existing_batches))
+ if not batch_nos:
+ return
+
batch_nos_details = []
user = frappe.session.user
for batch_no in batch_nos:
@@ -1156,7 +1160,7 @@ def get_filters_for_bundle(item_code=None, docstatus=None, voucher_no=None, name
@frappe.whitelist()
-def add_serial_batch_ledgers(entries, child_row, doc, warehouse) -> object:
+def add_serial_batch_ledgers(entries, child_row, doc, warehouse, do_not_save=False) -> object:
if isinstance(child_row, str):
child_row = frappe._dict(parse_json(child_row))
@@ -1170,20 +1174,23 @@ def add_serial_batch_ledgers(entries, child_row, doc, warehouse) -> object:
if frappe.db.exists("Serial and Batch Bundle", child_row.serial_and_batch_bundle):
sb_doc = update_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse)
else:
- sb_doc = create_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse)
+ sb_doc = create_serial_batch_no_ledgers(
+ entries, child_row, parent_doc, warehouse, do_not_save=do_not_save
+ )
return sb_doc
-def create_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse=None) -> object:
+def create_serial_batch_no_ledgers(
+ entries, child_row, parent_doc, warehouse=None, do_not_save=False
+) -> object:
warehouse = warehouse or (
child_row.rejected_warehouse if child_row.is_rejected else child_row.warehouse
)
- type_of_transaction = child_row.type_of_transaction
+ type_of_transaction = get_type_of_transaction(parent_doc, child_row)
if parent_doc.get("doctype") == "Stock Entry":
- type_of_transaction = "Outward" if child_row.s_warehouse else "Inward"
warehouse = warehouse or child_row.s_warehouse or child_row.t_warehouse
doc = frappe.get_doc(
@@ -1214,13 +1221,30 @@ def create_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse=Non
doc.save()
- frappe.db.set_value(child_row.doctype, child_row.name, "serial_and_batch_bundle", doc.name)
+ if do_not_save:
+ frappe.db.set_value(child_row.doctype, child_row.name, "serial_and_batch_bundle", doc.name)
frappe.msgprint(_("Serial and Batch Bundle created"), alert=True)
return doc
+def get_type_of_transaction(parent_doc, child_row):
+ type_of_transaction = child_row.type_of_transaction
+ if parent_doc.get("doctype") == "Stock Entry":
+ type_of_transaction = "Outward" if child_row.s_warehouse else "Inward"
+
+ if not type_of_transaction:
+ type_of_transaction = "Outward"
+ if parent_doc.get("doctype") in ["Purchase Receipt", "Purchase Invoice"]:
+ type_of_transaction = "Inward"
+
+ if parent_doc.get("is_return"):
+ type_of_transaction = "Inward" if type_of_transaction == "Outward" else "Outward"
+
+ return type_of_transaction
+
+
def update_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse=None) -> object:
doc = frappe.get_doc("Serial and Batch Bundle", child_row.serial_and_batch_bundle)
doc.voucher_detail_no = child_row.name
@@ -1247,6 +1271,25 @@ def update_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse=Non
return doc
+@frappe.whitelist()
+def update_serial_or_batch(bundle_id, serial_no=None, batch_no=None):
+ if batch_no and not serial_no:
+ if qty := frappe.db.get_value(
+ "Serial and Batch Entry", {"parent": bundle_id, "batch_no": batch_no}, "qty"
+ ):
+ frappe.db.set_value(
+ "Serial and Batch Entry", {"parent": bundle_id, "batch_no": batch_no}, "qty", qty + 1
+ )
+ return
+
+ doc = frappe.get_cached_doc("Serial and Batch Bundle", bundle_id)
+ if not serial_no and not batch_no:
+ return
+
+ doc.append("entries", {"serial_no": serial_no, "batch_no": batch_no, "qty": 1})
+ doc.save(ignore_permissions=True)
+
+
def get_serial_and_batch_ledger(**kwargs):
kwargs = frappe._dict(kwargs)
@@ -2032,3 +2075,8 @@ def get_stock_ledgers_batches(kwargs):
@frappe.whitelist()
def get_batch_no_from_serial_no(serial_no):
return frappe.get_cached_value("Serial No", serial_no, "batch_no")
+
+
+@frappe.whitelist()
+def is_duplicate_serial_no(bundle_id, serial_no):
+ return frappe.db.exists("Serial and Batch Entry", {"parent": bundle_id, "serial_no": serial_no})
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 19757479a5a..0d453fb8418 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
@@ -10,6 +10,8 @@ from frappe.utils import add_days, add_to_date, flt, nowdate, nowtime, today
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
add_serial_batch_ledgers,
+ make_batch_nos,
+ make_serial_nos,
)
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@@ -481,6 +483,38 @@ class TestSerialandBatchBundle(FrappeTestCase):
docstatus = frappe.db.get_value("Serial and Batch Bundle", bundle, "docstatus")
self.assertEqual(docstatus, 2)
+ def test_batch_duplicate_entry(self):
+ item_code = make_item(properties={"has_batch_no": 1}).name
+
+ batch_id = "TEST-BATTCCH-VAL-00001"
+ batch_nos = [{"batch_no": batch_id, "qty": 1}]
+
+ make_batch_nos(item_code, batch_nos)
+ self.assertTrue(frappe.db.exists("Batch", batch_id))
+
+ batch_id = "TEST-BATTCCH-VAL-00001"
+ batch_nos = [{"batch_no": batch_id, "qty": 1}]
+
+ # Shouldn't throw duplicate entry error
+ make_batch_nos(item_code, batch_nos)
+ self.assertTrue(frappe.db.exists("Batch", batch_id))
+
+ def test_serial_no_duplicate_entry(self):
+ item_code = make_item(properties={"has_serial_no": 1}).name
+
+ serial_no_id = "TEST-SNID-VAL-00001"
+ serial_nos = [{"serial_no": serial_no_id, "qty": 1}]
+
+ make_serial_nos(item_code, serial_nos)
+ self.assertTrue(frappe.db.exists("Serial No", serial_no_id))
+
+ serial_no_id = "TEST-SNID-VAL-00001"
+ serial_nos = [{"batch_no": serial_no_id, "qty": 1}]
+
+ # Shouldn't throw duplicate entry error
+ make_serial_nos(item_code, serial_nos)
+ self.assertTrue(frappe.db.exists("Serial No", serial_no_id))
+
def get_batch_from_bundle(bundle):
from erpnext.stock.serial_batch_bundle import get_batch_nos
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index d35288a91cb..9e6ef0f06a7 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -24,6 +24,7 @@ from frappe.utils import (
import erpnext
from erpnext.accounts.general_ledger import process_gl_map
+from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals
from erpnext.manufacturing.doctype.bom.bom import (
add_additional_cost,
@@ -208,7 +209,6 @@ class StockEntry(StockController):
self.validate_bom()
self.set_process_loss_qty()
self.validate_purchase_order()
- self.validate_subcontracting_order()
if self.purpose in ("Manufacture", "Repack"):
self.mark_finished_and_scrap_items()
@@ -274,6 +274,7 @@ class StockEntry(StockController):
return False
def on_submit(self):
+ self.validate_closed_subcontracting_order()
self.update_stock_ledger()
self.update_work_order()
self.validate_subcontract_order()
@@ -294,6 +295,7 @@ class StockEntry(StockController):
self.set_material_request_transfer_status("Completed")
def on_cancel(self):
+ self.validate_closed_subcontracting_order()
self.update_subcontract_order_supplied_items()
self.update_subcontracting_order_status()
@@ -1197,19 +1199,9 @@ class StockEntry(StockController):
)
)
- def validate_subcontracting_order(self):
- if self.get("subcontracting_order") and self.purpose in [
- "Send to Subcontractor",
- "Material Transfer",
- ]:
- sco_status = frappe.db.get_value("Subcontracting Order", self.subcontracting_order, "status")
-
- if sco_status == "Closed":
- frappe.throw(
- _("Cannot create Stock Entry against a closed Subcontracting Order {0}.").format(
- self.subcontracting_order
- )
- )
+ def validate_closed_subcontracting_order(self):
+ if self.get("subcontracting_order"):
+ check_on_hold_or_closed_status("Subcontracting Order", self.subcontracting_order)
def mark_finished_and_scrap_items(self):
if self.purpose != "Repack" and any(
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 ab39adee5c0..69db4f57726 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -111,16 +111,20 @@ class StockLedgerEntry(Document):
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"company": self.company,
+ "sle": self.name,
}
)
sle = get_previous_sle(kwargs, extra_cond=extra_cond)
+ qty_after_transaction = 0.0
+ flt_precision = cint(frappe.db.get_default("float_precision")) or 2
if sle:
- flt_precision = cint(frappe.db.get_default("float_precision")) or 2
- diff = sle.qty_after_transaction + flt(self.actual_qty)
- diff = flt(diff, flt_precision)
- if diff < 0 and abs(diff) > 0.0001:
- self.throw_validation_error(diff, dimensions)
+ qty_after_transaction = sle.qty_after_transaction
+
+ diff = qty_after_transaction + flt(self.actual_qty)
+ diff = flt(diff, flt_precision)
+ if diff < 0 and abs(diff) > 0.0001:
+ self.throw_validation_error(diff, dimensions)
def throw_validation_error(self, diff, dimensions):
dimension_msg = _(", with the inventory {0}: {1}").format(
diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
index 39df2279cd2..4cfe5d817e6 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -209,7 +209,7 @@ class SerialBatchBundle:
frappe.db.set_value(
"Serial and Batch Bundle",
{"voucher_no": self.sle.voucher_no, "voucher_type": self.sle.voucher_type},
- {"is_cancelled": 1, "voucher_no": ""},
+ {"is_cancelled": 1},
)
if self.sle.serial_and_batch_bundle:
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index bd0d4697c94..4b0e2845c44 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -591,6 +591,13 @@ def scan_barcode(search_value: str) -> BarcodeScanResult:
as_dict=True,
)
if batch_no_data:
+ if frappe.get_cached_value("Item", batch_no_data.item_code, "has_serial_no"):
+ frappe.throw(
+ _(
+ "Batch No {0} is linked with Item {1} which has serial no. Please scan serial no instead."
+ ).format(search_value, batch_no_data.item_code)
+ )
+
_update_item_info(batch_no_data)
set_cache(batch_no_data)
return batch_no_data
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js
index 587a3b4ebfa..4c8a0ad60ed 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js
@@ -101,9 +101,32 @@ frappe.ui.form.on('Subcontracting Order', {
},
refresh: function (frm) {
+ if (frm.doc.docstatus == 1 && frm.has_perm("submit")) {
+ if (frm.doc.status == "Closed") {
+ frm.add_custom_button(__('Re-open'), () => frm.events.update_subcontracting_order_status(frm), __("Status"));
+ } else if(flt(frm.doc.per_received, 2) < 100) {
+ frm.add_custom_button(__('Close'), () => frm.events.update_subcontracting_order_status(frm, "Closed"), __("Status"));
+ }
+ }
+
frm.trigger('get_materials_from_supplier');
},
+ update_subcontracting_order_status(frm, status) {
+ frappe.call({
+ method: "erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.update_subcontracting_order_status",
+ args: {
+ sco: frm.doc.name,
+ status: status,
+ },
+ callback: function (r) {
+ if (!r.exc) {
+ frm.reload_doc();
+ }
+ },
+ });
+ },
+
get_materials_from_supplier: function (frm) {
let sco_rm_details = [];
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json
index 28c52c9272d..507e23365cc 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json
@@ -370,7 +370,7 @@
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
- "options": "Draft\nOpen\nPartially Received\nCompleted\nMaterial Transferred\nPartial Material Transferred\nCancelled",
+ "options": "Draft\nOpen\nPartially Received\nCompleted\nMaterial Transferred\nPartial Material Transferred\nCancelled\nClosed",
"print_hide": 1,
"read_only": 1,
"reqd": 1,
@@ -454,7 +454,7 @@
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
- "modified": "2023-06-03 16:18:17.782538",
+ "modified": "2024-01-03 20:56:04.670380",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Order",
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
index 0fe8c13efbd..daccbbbd0f9 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
@@ -7,7 +7,7 @@ from frappe.model.mapper import get_mapped_doc
from frappe.utils import flt
from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created
-from erpnext.buying.doctype.purchase_order.purchase_order import update_status as update_po_status
+from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.subcontracting_controller import SubcontractingController
from erpnext.stock.stock_balance import update_bin_qty
from erpnext.stock.utils import get_bin
@@ -68,6 +68,7 @@ class SubcontractingOrder(SubcontractingController):
"Material Transferred",
"Partial Material Transferred",
"Cancelled",
+ "Closed",
]
supplied_items: DF.Table[SubcontractingOrderSuppliedItem]
supplier: DF.Link
@@ -112,16 +113,10 @@ class SubcontractingOrder(SubcontractingController):
def on_submit(self):
self.update_prevdoc_status()
- self.update_requested_qty()
- self.update_ordered_qty_for_subcontracting()
- self.update_reserved_qty_for_subcontracting()
self.update_status()
def on_cancel(self):
self.update_prevdoc_status()
- self.update_requested_qty()
- self.update_ordered_qty_for_subcontracting()
- self.update_reserved_qty_for_subcontracting()
self.update_status()
def validate_purchase_order_for_subcontracting(self):
@@ -277,6 +272,9 @@ class SubcontractingOrder(SubcontractingController):
self.set_missing_values()
def update_status(self, status=None, update_modified=True):
+ if self.status == "Closed" and self.status != status:
+ check_on_hold_or_closed_status("Purchase Order", self.purchase_order)
+
if self.docstatus >= 1 and not status:
if self.docstatus == 1:
if self.status == "Draft":
@@ -285,11 +283,6 @@ class SubcontractingOrder(SubcontractingController):
status = "Completed"
elif self.per_received > 0 and self.per_received < 100:
status = "Partially Received"
- for item in self.supplied_items:
- if not item.returned_qty or (item.supplied_qty - item.consumed_qty - item.returned_qty) > 0:
- break
- else:
- status = "Closed"
else:
total_required_qty = total_supplied_qty = 0
for item in self.supplied_items:
@@ -304,13 +297,12 @@ class SubcontractingOrder(SubcontractingController):
elif self.docstatus == 2:
status = "Cancelled"
- if status:
- frappe.db.set_value(
- "Subcontracting Order", self.name, "status", status, update_modified=update_modified
- )
+ if status and self.status != status:
+ self.db_set("status", status, update_modified=update_modified)
- if status == "Closed":
- update_po_status("Closed", self.purchase_order)
+ self.update_requested_qty()
+ self.update_ordered_qty_for_subcontracting()
+ self.update_reserved_qty_for_subcontracting()
@frappe.whitelist()
@@ -357,8 +349,8 @@ def get_mapped_subcontracting_receipt(source_name, target_doc=None):
@frappe.whitelist()
-def update_subcontracting_order_status(sco):
+def update_subcontracting_order_status(sco, status=None):
if isinstance(sco, str):
sco = frappe.get_doc("Subcontracting Order", sco)
- sco.update_status()
+ sco.update_status(status)
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js
index 7ca12642c5f..ec54944a849 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js
@@ -10,7 +10,7 @@ frappe.listview_settings['Subcontracting Order'] = {
"Completed": "green",
"Partial Material Transferred": "purple",
"Material Transferred": "blue",
- "Closed": "red",
+ "Closed": "green",
"Cancelled": "red",
};
return [__(doc.status), status_colors[doc.status], "status,=," + doc.status];
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
index 37dabf1bfbe..6c0ee45d9c5 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
+++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
@@ -95,14 +95,14 @@ class TestSubcontractingOrder(FrappeTestCase):
self.assertEqual(sco.status, "Partially Received")
# Closed
- ste = get_materials_from_supplier(sco.name, [d.name for d in sco.supplied_items])
- ste.save()
- ste.submit()
- sco.load_from_db()
+ sco.update_status("Closed")
self.assertEqual(sco.status, "Closed")
- ste.cancel()
- sco.load_from_db()
+ scr = make_subcontracting_receipt(sco.name)
+ scr.save()
+ self.assertRaises(frappe.exceptions.ValidationError, scr.submit)
+ sco.update_status()
self.assertEqual(sco.status, "Partially Received")
+ scr.cancel()
# Completed
scr = make_subcontracting_receipt(sco.name)
@@ -564,7 +564,6 @@ class TestSubcontractingOrder(FrappeTestCase):
sco.load_from_db()
- self.assertEqual(sco.status, "Closed")
self.assertEqual(sco.supplied_items[0].returned_qty, 5)
def test_ordered_qty_for_subcontracting_order(self):
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
index 575c4eda731..05357999a1b 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
@@ -93,7 +93,8 @@ frappe.ui.form.on('Subcontracting Receipt', {
get_query_filters: {
docstatus: 1,
per_received: ['<', 100],
- company: frm.doc.company
+ company: frm.doc.company,
+ status: ['!=', 'Closed'],
}
});
}, __('Get Items From'));
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
index fc1b697a8e0..475b6030780 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
@@ -8,6 +8,7 @@ from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate
import erpnext
from erpnext.accounts.utils import get_account_currency
+from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.subcontracting_controller import SubcontractingController
from erpnext.stock.stock_ledger import get_valuation_rate
@@ -142,6 +143,7 @@ class SubcontractingReceipt(SubcontractingController):
self.get_current_stock()
def on_submit(self):
+ self.validate_closed_subcontracting_order()
self.validate_available_qty_for_consumption()
self.update_status_updater_args()
self.update_prevdoc_status()
@@ -165,6 +167,7 @@ class SubcontractingReceipt(SubcontractingController):
"Repost Item Valuation",
"Serial and Batch Bundle",
)
+ self.validate_closed_subcontracting_order()
self.update_status_updater_args()
self.update_prevdoc_status()
self.set_consumed_qty_in_subcontract_order()
@@ -175,6 +178,11 @@ class SubcontractingReceipt(SubcontractingController):
self.update_status()
self.delete_auto_created_batches()
+ def validate_closed_subcontracting_order(self):
+ for item in self.items:
+ if item.subcontracting_order:
+ check_on_hold_or_closed_status("Subcontracting Order", item.subcontracting_order)
+
def validate_items_qty(self):
for item in self.items:
if not (item.qty or item.rejected_qty):
diff --git a/erpnext/utilities/bulk_transaction.py b/erpnext/utilities/bulk_transaction.py
index 679d5bd348e..9678488a261 100644
--- a/erpnext/utilities/bulk_transaction.py
+++ b/erpnext/utilities/bulk_transaction.py
@@ -15,18 +15,15 @@ def transaction_processing(data, from_doctype, to_doctype):
length_of_data = len(deserialized_data)
- if length_of_data > 10:
- frappe.msgprint(
- _("Started a background job to create {1} {0}").format(to_doctype, length_of_data)
- )
- frappe.enqueue(
- job,
- deserialized_data=deserialized_data,
- from_doctype=from_doctype,
- to_doctype=to_doctype,
- )
- else:
- job(deserialized_data, from_doctype, to_doctype)
+ frappe.msgprint(
+ _("Started a background job to create {1} {0}").format(to_doctype, length_of_data)
+ )
+ frappe.enqueue(
+ job,
+ deserialized_data=deserialized_data,
+ from_doctype=from_doctype,
+ to_doctype=to_doctype,
+ )
@frappe.whitelist()