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/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 65d4595fcd5..8396783ffa1 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -521,7 +521,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 cc1109b7dc8..4a9153a1ebc 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -925,17 +925,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" @@ -975,17 +964,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/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 1a04841fd52..b236b447d57 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -890,8 +890,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 c24d28b3249..c83969b34bc 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( diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 37fbd184fe1..f711577abbe 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -519,10 +519,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 91ae62bfad7..cdccf81507b 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", @@ -583,7 +583,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 f83d7071aa7..3db4a8d18bd 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -247,7 +247,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 @@ -260,7 +265,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: @@ -428,10 +433,7 @@ class Asset(AccountsController): n == 0 and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata) and not self.opening_accumulated_depreciation - and get_updated_rate_of_depreciation_for_wdv_and_dd( - self, value_after_depreciation, finance_book, False - ) - == finance_book.rate_of_depreciation + and not self.flags.wdv_it_act_applied ): from_date = add_days( self.available_for_use_date, -1 @@ -1378,26 +1380,16 @@ def get_depreciation_amount( asset, fb_row, schedule_idx, number_of_pending_depreciations ) else: - rate_of_depreciation = get_updated_rate_of_depreciation_for_wdv_and_dd( - asset, depreciable_value, fb_row - ) return get_wdv_or_dd_depr_amount( + asset, + fb_row, depreciable_value, - rate_of_depreciation, - fb_row.frequency_of_depreciation, schedule_idx, prev_depreciation_amount, has_wdv_or_dd_non_yearly_pro_rata, ) -@erpnext.allow_regional -def get_updated_rate_of_depreciation_for_wdv_and_dd( - asset, depreciable_value, fb_row, show_msg=True -): - return fb_row.rate_of_depreciation - - def get_straight_line_or_manual_depr_amount( asset, row, schedule_idx, number_of_pending_depreciations ): @@ -1532,30 +1524,53 @@ def get_asset_shift_factors_map(): return dict(frappe.db.get_all("Asset Shift Factor", ["shift_name", "shift_factor"], as_list=True)) +@erpnext.allow_regional def get_wdv_or_dd_depr_amount( + asset, + fb_row, depreciable_value, - rate_of_depreciation, - frequency_of_depreciation, schedule_idx, prev_depreciation_amount, has_wdv_or_dd_non_yearly_pro_rata, ): - if cint(frequency_of_depreciation) == 12: - return flt(depreciable_value) * (flt(rate_of_depreciation) / 100) + return get_default_wdv_or_dd_depr_amount( + asset, + fb_row, + depreciable_value, + schedule_idx, + prev_depreciation_amount, + has_wdv_or_dd_non_yearly_pro_rata, + ) + + +def get_default_wdv_or_dd_depr_amount( + asset, + fb_row, + depreciable_value, + schedule_idx, + prev_depreciation_amount, + has_wdv_or_dd_non_yearly_pro_rata, +): + if cint(fb_row.frequency_of_depreciation) == 12: + return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100) else: if has_wdv_or_dd_non_yearly_pro_rata: if schedule_idx == 0: - return flt(depreciable_value) * (flt(rate_of_depreciation) / 100) - elif schedule_idx % (12 / cint(frequency_of_depreciation)) == 1: + return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100) + elif schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 1: return ( - flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200) + flt(depreciable_value) + * flt(fb_row.frequency_of_depreciation) + * (flt(fb_row.rate_of_depreciation) / 1200) ) else: return prev_depreciation_amount else: - if schedule_idx % (12 / cint(frequency_of_depreciation)) == 0: + if schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 0: return ( - flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200) + flt(depreciable_value) + * flt(fb_row.frequency_of_depreciation) + * (flt(fb_row.rate_of_depreciation) / 1200) ) else: return prev_depreciation_amount diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 6f76955e12d..7eb600025e0 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -19,6 +19,7 @@ from frappe.utils import ( from frappe.utils.data import get_link_to_form from frappe.utils.user import get_users_with_role +import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_checks_for_pl_and_bs_accounts, ) @@ -473,6 +474,13 @@ def depreciate_asset(asset, date): make_depreciation_entry(asset.name, date) + cancel_depreciation_entries(asset, date) + + +@erpnext.allow_regional +def cancel_depreciation_entries(asset, date): + pass + def reset_depreciation_schedule(asset, date): if not asset.calculate_depreciation: diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index 8d351412ca8..2a204e11477 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -67,12 +67,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_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json index 172b81d98b0..9e5dd3e1a68 100644 --- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json +++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json @@ -90,7 +90,6 @@ }, { "default": "0", - "depends_on": "eval:doc.depreciation_method == \"Straight Line\" || doc.depreciation_method == \"Manual\"", "fieldname": "daily_prorata_based", "fieldtype": "Check", "label": "Depreciate based on daily pro-rata" @@ -106,7 +105,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-29 03:53:03.591098", + "modified": "2023-12-29 08:49:39.876439", "modified_by": "Administrator", "module": "Assets", "name": "Asset Finance Book", diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 0af93bfc902..5be70288c56 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -188,7 +188,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-11-24 10:55:51.287327", + "modified": "2024-01-05 15:26:02.320942", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", @@ -212,10 +212,45 @@ "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", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 8e9b2289079..a2b7a65fce0 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -730,11 +730,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( { @@ -750,7 +747,7 @@ class BuyingController(SubcontractingController): "calculate_depreciation": 1, "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, "cost_center": row.cost_center, diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index efeedc14db6..f1d1e7eaebd 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -8,7 +8,7 @@ from frappe.model.meta import get_field_precision from frappe.utils import flt, format_datetime, get_datetime import erpnext -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 d3157e7037b..d55aeeacc08 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -422,6 +422,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/hooks.py b/erpnext/hooks.py index 054f9dedabe..97d71a5f2a3 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -1,5 +1,3 @@ -from frappe import _ - app_name = "erpnext" app_title = "ERPNext" app_publisher = "Frappe Technologies Pvt. Ltd." @@ -92,7 +90,7 @@ website_route_rules = [ { "from_route": "/orders/", "to_route": "order", - "defaults": {"doctype": "Sales Order", "parents": [{"label": _("Orders"), "route": "orders"}]}, + "defaults": {"doctype": "Sales Order", "parents": [{"label": "Orders", "route": "orders"}]}, }, {"from_route": "/invoices", "to_route": "Sales Invoice"}, { @@ -100,7 +98,7 @@ website_route_rules = [ "to_route": "order", "defaults": { "doctype": "Sales Invoice", - "parents": [{"label": _("Invoices"), "route": "invoices"}], + "parents": [{"label": "Invoices", "route": "invoices"}], }, }, {"from_route": "/supplier-quotations", "to_route": "Supplier Quotation"}, @@ -109,7 +107,7 @@ website_route_rules = [ "to_route": "order", "defaults": { "doctype": "Supplier Quotation", - "parents": [{"label": _("Supplier Quotation"), "route": "supplier-quotations"}], + "parents": [{"label": "Supplier Quotation", "route": "supplier-quotations"}], }, }, {"from_route": "/purchase-orders", "to_route": "Purchase Order"}, @@ -118,7 +116,7 @@ website_route_rules = [ "to_route": "order", "defaults": { "doctype": "Purchase Order", - "parents": [{"label": _("Purchase Order"), "route": "purchase-orders"}], + "parents": [{"label": "Purchase Order", "route": "purchase-orders"}], }, }, {"from_route": "/purchase-invoices", "to_route": "Purchase Invoice"}, @@ -127,7 +125,7 @@ website_route_rules = [ "to_route": "order", "defaults": { "doctype": "Purchase Invoice", - "parents": [{"label": _("Purchase Invoice"), "route": "purchase-invoices"}], + "parents": [{"label": "Purchase Invoice", "route": "purchase-invoices"}], }, }, {"from_route": "/quotations", "to_route": "Quotation"}, @@ -136,7 +134,7 @@ website_route_rules = [ "to_route": "order", "defaults": { "doctype": "Quotation", - "parents": [{"label": _("Quotations"), "route": "quotations"}], + "parents": [{"label": "Quotations", "route": "quotations"}], }, }, {"from_route": "/shipments", "to_route": "Delivery Note"}, @@ -145,7 +143,7 @@ website_route_rules = [ "to_route": "order", "defaults": { "doctype": "Delivery Note", - "parents": [{"label": _("Shipments"), "route": "shipments"}], + "parents": [{"label": "Shipments", "route": "shipments"}], }, }, {"from_route": "/rfq", "to_route": "Request for Quotation"}, @@ -154,14 +152,14 @@ website_route_rules = [ "to_route": "rfq", "defaults": { "doctype": "Request for Quotation", - "parents": [{"label": _("Request for Quotation"), "route": "rfq"}], + "parents": [{"label": "Request for Quotation", "route": "rfq"}], }, }, {"from_route": "/addresses", "to_route": "Address"}, { "from_route": "/addresses/", "to_route": "addresses", - "defaults": {"doctype": "Address", "parents": [{"label": _("Addresses"), "route": "addresses"}]}, + "defaults": {"doctype": "Address", "parents": [{"label": "Addresses", "route": "addresses"}]}, }, {"from_route": "/boms", "to_route": "BOM"}, {"from_route": "/timesheets", "to_route": "Timesheet"}, @@ -171,78 +169,78 @@ website_route_rules = [ "to_route": "material_request_info", "defaults": { "doctype": "Material Request", - "parents": [{"label": _("Material Request"), "route": "material-requests"}], + "parents": [{"label": "Material Request", "route": "material-requests"}], }, }, {"from_route": "/project", "to_route": "Project"}, ] standard_portal_menu_items = [ - {"title": _("Projects"), "route": "/project", "reference_doctype": "Project"}, + {"title": "Projects", "route": "/project", "reference_doctype": "Project"}, { - "title": _("Request for Quotations"), + "title": "Request for Quotations", "route": "/rfq", "reference_doctype": "Request for Quotation", "role": "Supplier", }, { - "title": _("Supplier Quotation"), + "title": "Supplier Quotation", "route": "/supplier-quotations", "reference_doctype": "Supplier Quotation", "role": "Supplier", }, { - "title": _("Purchase Orders"), + "title": "Purchase Orders", "route": "/purchase-orders", "reference_doctype": "Purchase Order", "role": "Supplier", }, { - "title": _("Purchase Invoices"), + "title": "Purchase Invoices", "route": "/purchase-invoices", "reference_doctype": "Purchase Invoice", "role": "Supplier", }, { - "title": _("Quotations"), + "title": "Quotations", "route": "/quotations", "reference_doctype": "Quotation", "role": "Customer", }, { - "title": _("Orders"), + "title": "Orders", "route": "/orders", "reference_doctype": "Sales Order", "role": "Customer", }, { - "title": _("Invoices"), + "title": "Invoices", "route": "/invoices", "reference_doctype": "Sales Invoice", "role": "Customer", }, { - "title": _("Shipments"), + "title": "Shipments", "route": "/shipments", "reference_doctype": "Delivery Note", "role": "Customer", }, - {"title": _("Issues"), "route": "/issues", "reference_doctype": "Issue", "role": "Customer"}, - {"title": _("Addresses"), "route": "/addresses", "reference_doctype": "Address"}, + {"title": "Issues", "route": "/issues", "reference_doctype": "Issue", "role": "Customer"}, + {"title": "Addresses", "route": "/addresses", "reference_doctype": "Address"}, { - "title": _("Timesheets"), + "title": "Timesheets", "route": "/timesheets", "reference_doctype": "Timesheet", "role": "Customer", }, - {"title": _("Newsletter"), "route": "/newsletters", "reference_doctype": "Newsletter"}, + {"title": "Newsletter", "route": "/newsletters", "reference_doctype": "Newsletter"}, { - "title": _("Material Request"), + "title": "Material Request", "route": "/material-requests", "reference_doctype": "Material Request", "role": "Customer", }, - {"title": _("Appointment Booking"), "route": "/book_appointment"}, + {"title": "Appointment Booking", "route": "/book_appointment"}, ] default_roles = [ diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py index 95e2d694a58..8e4af3ac172 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py @@ -80,7 +80,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() @@ -94,12 +94,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: @@ -195,8 +194,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)) @@ -392,16 +389,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): - serial_nos = get_serial_nos(target.serial_no) - if len(serial_nos) == 1: - target.serial_no = serial_nos[0] - else: - target.serial_no = "" + if source.item_reference: + if serial_nos := frappe.db.get_value( + "Maintenance Schedule Item", source.item_reference, "serial_no" + ): + serial_nos = serial_nos.split("\n") + + if len(serial_nos) == 1: + target.serial_no = serial_nos[0] + else: + target.serial_no = "" doclist = get_mapped_doc( "Maintenance Schedule", @@ -413,10 +422,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 0d319bf7424..a0974e99040 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py @@ -23,20 +23,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() @@ -49,6 +68,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 @@ -56,6 +76,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/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 ee2e58bfb46..767b4ccbb1a 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -570,6 +570,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") @@ -578,12 +582,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 3695ae9d9d8..4f6280ade8f 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -661,7 +661,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(): @@ -1511,6 +1511,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/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index e63ac144f64..9e52befd22e 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -389,6 +389,9 @@ def make_sales_invoice(source_name, item_code=None, customer=None, currency=None "timesheets", { "time_sheet": timesheet.name, + "project_name": time_log.project_name, + "from_time": time_log.from_time, + "to_time": time_log.to_time, "billing_hours": time_log.billing_hours, "billing_amount": time_log.billing_amount, "timesheet_detail": time_log.name, diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 0f291e1eba7..48bde9dbc98 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -739,7 +739,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); } diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index fea27da61b2..b7fcadb1eca 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -706,7 +706,9 @@ def make_contact(args, is_primary_contact=1): else: values.update( { - "first_name": args.get("customer_name"), + "first_name": args.get("customer_name") + if args.doctype == "Customer" + else args.get("supplier_name"), "company_name": args.get(party_name_key), } ) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 88e24ad542a..539960a508f 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -288,15 +288,16 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): ) # 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 cbc11ed02af..bec24fe5e99 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -432,17 +432,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/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 2f7a2dd70b7..e905f673365 100644 --- a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py +++ b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py @@ -44,7 +44,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 a931ee2c254..9f2c0be75d0 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1435,6 +1435,25 @@ class TestDeliveryNote(FrappeTestCase): returned_dn.reload() self.assertAlmostEqual(returned_dn.items[0].incoming_rate, 200.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 257d18fc33a..a7ead064c97 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py @@ -281,7 +281,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 531bc3f109f..59389c7d7df 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 7afc031d87d..b9256c8ee56 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -203,6 +203,7 @@ "label": "Allow Alternative Item" }, { + "allow_in_quick_entry": 1, "bold": 1, "default": "1", "depends_on": "eval:!doc.is_fixed_asset", @@ -240,6 +241,7 @@ "label": "Standard Selling Rate" }, { + "allow_in_quick_entry": 1, "default": "0", "fieldname": "is_fixed_asset", "fieldtype": "Check", @@ -247,6 +249,7 @@ "set_only_once": 1 }, { + "allow_in_quick_entry": 1, "depends_on": "is_fixed_asset", "fieldname": "asset_category", "fieldtype": "Link", @@ -897,7 +900,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 7f0dc2df9f3..3511cec2fd7 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -167,7 +167,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() @@ -208,20 +209,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 032e2fec822..d7cefe36473 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -606,7 +606,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: @@ -738,11 +738,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/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 921b04aab8c..1d52b17149b 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -71,16 +71,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/utilities/bulk_transaction.py b/erpnext/utilities/bulk_transaction.py index 7577f0a379e..b519617435f 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()