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()