Merge branch 'develop' into PACKING-SLIP-FOR-DN-PACKED-ITEMS
This commit is contained in:
commit
2813042936
@ -1180,7 +1180,12 @@ class SalesInvoice(SellingController):
|
||||
|
||||
if self.is_return:
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_regain(
|
||||
asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name")
|
||||
asset,
|
||||
item.base_net_amount,
|
||||
item.finance_book,
|
||||
self.get("doctype"),
|
||||
self.get("name"),
|
||||
self.get("posting_date"),
|
||||
)
|
||||
asset.db_set("disposal_date", None)
|
||||
|
||||
@ -1208,7 +1213,12 @@ class SalesInvoice(SellingController):
|
||||
asset.reload()
|
||||
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
|
||||
asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name")
|
||||
asset,
|
||||
item.base_net_amount,
|
||||
item.finance_book,
|
||||
self.get("doctype"),
|
||||
self.get("name"),
|
||||
self.get("posting_date"),
|
||||
)
|
||||
asset.db_set("disposal_date", self.posting_date)
|
||||
|
||||
|
41
erpnext/accounts/form_tour/sales_invoice/sales_invoice.json
Normal file
41
erpnext/accounts/form_tour/sales_invoice/sales_invoice.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"creation": "2023-05-23 09:58:17.235916",
|
||||
"docstatus": 0,
|
||||
"doctype": "Form Tour",
|
||||
"first_document": 0,
|
||||
"idx": 0,
|
||||
"include_name_field": 0,
|
||||
"is_standard": 1,
|
||||
"modified": "2023-05-23 13:10:56.227127",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
"owner": "Administrator",
|
||||
"reference_doctype": "Sales Invoice",
|
||||
"save_on_complete": 1,
|
||||
"steps": [
|
||||
{
|
||||
"description": "Select a customer for whom this invoice is being prepared.",
|
||||
"fieldname": "customer",
|
||||
"fieldtype": "Link",
|
||||
"has_next_condition": 1,
|
||||
"is_table_field": 0,
|
||||
"label": "Customer",
|
||||
"next_step_condition": "eval: doc.customer",
|
||||
"position": "Right",
|
||||
"title": "Select Customer"
|
||||
},
|
||||
{
|
||||
"child_doctype": "Sales Invoice Item",
|
||||
"description": "Select item that you have sold along with quantity and rate.",
|
||||
"fieldname": "items",
|
||||
"fieldtype": "Table",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"parent_fieldname": "items",
|
||||
"position": "Top",
|
||||
"title": "Select Item"
|
||||
}
|
||||
],
|
||||
"title": "Sales Invoice"
|
||||
}
|
@ -307,7 +307,7 @@ def scrap_asset(asset_name):
|
||||
je.company = asset.company
|
||||
je.remark = "Scrap Entry for asset {0}".format(asset_name)
|
||||
|
||||
for entry in get_gl_entries_on_asset_disposal(asset):
|
||||
for entry in get_gl_entries_on_asset_disposal(asset, date):
|
||||
entry.update({"reference_type": "Asset", "reference_name": asset_name})
|
||||
je.append("accounts", entry)
|
||||
|
||||
@ -434,8 +434,11 @@ def disposal_happens_in_the_future(posting_date_of_disposal):
|
||||
|
||||
|
||||
def get_gl_entries_on_asset_regain(
|
||||
asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None
|
||||
asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None, date=None
|
||||
):
|
||||
if not date:
|
||||
date = getdate()
|
||||
|
||||
(
|
||||
fixed_asset_account,
|
||||
asset,
|
||||
@ -453,7 +456,7 @@ def get_gl_entries_on_asset_regain(
|
||||
"debit_in_account_currency": asset.gross_purchase_amount,
|
||||
"debit": asset.gross_purchase_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": date,
|
||||
},
|
||||
item=asset,
|
||||
),
|
||||
@ -463,7 +466,7 @@ def get_gl_entries_on_asset_regain(
|
||||
"credit_in_account_currency": accumulated_depr_amount,
|
||||
"credit": accumulated_depr_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": date,
|
||||
},
|
||||
item=asset,
|
||||
),
|
||||
@ -472,7 +475,7 @@ def get_gl_entries_on_asset_regain(
|
||||
profit_amount = abs(flt(value_after_depreciation)) - abs(flt(selling_amount))
|
||||
if profit_amount:
|
||||
get_profit_gl_entries(
|
||||
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center
|
||||
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center, date
|
||||
)
|
||||
|
||||
if voucher_type and voucher_no:
|
||||
@ -484,8 +487,11 @@ def get_gl_entries_on_asset_regain(
|
||||
|
||||
|
||||
def get_gl_entries_on_asset_disposal(
|
||||
asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None
|
||||
asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None, date=None
|
||||
):
|
||||
if not date:
|
||||
date = getdate()
|
||||
|
||||
(
|
||||
fixed_asset_account,
|
||||
asset,
|
||||
@ -503,7 +509,7 @@ def get_gl_entries_on_asset_disposal(
|
||||
"credit_in_account_currency": asset.gross_purchase_amount,
|
||||
"credit": asset.gross_purchase_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": date,
|
||||
},
|
||||
item=asset,
|
||||
),
|
||||
@ -513,7 +519,7 @@ def get_gl_entries_on_asset_disposal(
|
||||
"debit_in_account_currency": accumulated_depr_amount,
|
||||
"debit": accumulated_depr_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": date,
|
||||
},
|
||||
item=asset,
|
||||
),
|
||||
@ -522,7 +528,7 @@ def get_gl_entries_on_asset_disposal(
|
||||
profit_amount = flt(selling_amount) - flt(value_after_depreciation)
|
||||
if profit_amount:
|
||||
get_profit_gl_entries(
|
||||
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center
|
||||
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center, date
|
||||
)
|
||||
|
||||
if voucher_type and voucher_no:
|
||||
@ -556,8 +562,11 @@ def get_asset_details(asset, finance_book=None):
|
||||
|
||||
|
||||
def get_profit_gl_entries(
|
||||
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center
|
||||
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center, date=None
|
||||
):
|
||||
if not date:
|
||||
date = getdate()
|
||||
|
||||
debit_or_credit = "debit" if profit_amount < 0 else "credit"
|
||||
gl_entries.append(
|
||||
asset.get_gl_dict(
|
||||
@ -566,7 +575,7 @@ def get_profit_gl_entries(
|
||||
"cost_center": depreciation_cost_center,
|
||||
debit_or_credit: abs(profit_amount),
|
||||
debit_or_credit + "_in_account_currency": abs(profit_amount),
|
||||
"posting_date": getdate(),
|
||||
"posting_date": date,
|
||||
},
|
||||
item=asset,
|
||||
)
|
||||
|
@ -356,6 +356,83 @@ class TestAsset(AssetSetup):
|
||||
si.cancel()
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
|
||||
|
||||
def test_gle_made_by_asset_sale_for_existing_asset(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2020-04-01",
|
||||
purchase_date="2020-04-01",
|
||||
expected_value_after_useful_life=0,
|
||||
total_number_of_depreciations=5,
|
||||
number_of_depreciations_booked=2,
|
||||
frequency_of_depreciation=12,
|
||||
depreciation_start_date="2023-03-31",
|
||||
opening_accumulated_depreciation=24000,
|
||||
gross_purchase_amount=60000,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
expected_depr_values = [
|
||||
["2023-03-31", 12000, 36000],
|
||||
["2024-03-31", 12000, 48000],
|
||||
["2025-03-31", 12000, 60000],
|
||||
]
|
||||
|
||||
first_asset_depr_schedule = get_depr_schedule(asset.name, "Active")
|
||||
|
||||
for i, schedule in enumerate(first_asset_depr_schedule):
|
||||
self.assertEqual(getdate(expected_depr_values[i][0]), schedule.schedule_date)
|
||||
self.assertEqual(expected_depr_values[i][1], schedule.depreciation_amount)
|
||||
self.assertEqual(expected_depr_values[i][2], schedule.accumulated_depreciation_amount)
|
||||
|
||||
post_depreciation_entries(date="2023-03-31")
|
||||
|
||||
si = create_sales_invoice(
|
||||
item_code="Macbook Pro", asset=asset.name, qty=1, rate=40000, posting_date=getdate("2023-05-23")
|
||||
)
|
||||
asset.load_from_db()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
|
||||
|
||||
expected_values = [["2023-03-31", 12000, 36000], ["2023-05-23", 1742.47, 37742.47]]
|
||||
|
||||
second_asset_depr_schedule = get_depr_schedule(asset.name, "Active")
|
||||
|
||||
for i, schedule in enumerate(second_asset_depr_schedule):
|
||||
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
|
||||
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
|
||||
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
|
||||
self.assertTrue(schedule.journal_entry)
|
||||
|
||||
expected_gle = (
|
||||
(
|
||||
"_Test Accumulated Depreciations - _TC",
|
||||
37742.47,
|
||||
0.0,
|
||||
),
|
||||
(
|
||||
"_Test Fixed Asset - _TC",
|
||||
0.0,
|
||||
60000.0,
|
||||
),
|
||||
(
|
||||
"_Test Gain/Loss on Asset Disposal - _TC",
|
||||
0.0,
|
||||
17742.47,
|
||||
),
|
||||
("Debtors - _TC", 40000.0, 0.0),
|
||||
)
|
||||
|
||||
gle = frappe.db.sql(
|
||||
"""select account, debit, credit from `tabGL Entry`
|
||||
where voucher_type='Sales Invoice' and voucher_no = %s
|
||||
order by account""",
|
||||
si.name,
|
||||
)
|
||||
|
||||
self.assertSequenceEqual(gle, expected_gle)
|
||||
|
||||
def test_asset_with_maintenance_required_status_after_sale(self):
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
|
@ -443,6 +443,7 @@ class AssetCapitalization(StockController):
|
||||
item.get("finance_book") or self.get("finance_book"),
|
||||
self.get("doctype"),
|
||||
self.get("name"),
|
||||
self.get("posting_date"),
|
||||
)
|
||||
|
||||
asset.db_set("disposal_date", self.posting_date)
|
||||
|
@ -252,7 +252,10 @@ class AssetDepreciationSchedule(Document):
|
||||
|
||||
# if asset is being sold or scrapped
|
||||
if date_of_disposal:
|
||||
from_date = asset_doc.available_for_use_date
|
||||
from_date = add_months(
|
||||
getdate(asset_doc.available_for_use_date),
|
||||
(asset_doc.number_of_depreciations_booked * row.frequency_of_depreciation),
|
||||
)
|
||||
if self.depreciation_schedule:
|
||||
from_date = self.depreciation_schedule[-1].schedule_date
|
||||
|
||||
|
@ -157,7 +157,7 @@
|
||||
"party_account_currency",
|
||||
"inter_company_order_reference",
|
||||
"is_old_subcontracting_flow",
|
||||
"dashboard"
|
||||
"connections_tab"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -1171,7 +1171,6 @@
|
||||
"depends_on": "is_internal_supplier",
|
||||
"fieldname": "set_from_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Set From Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
@ -1185,12 +1184,6 @@
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "More Info"
|
||||
},
|
||||
{
|
||||
"fieldname": "dashboard",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Dashboard",
|
||||
"show_dashboard": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_7",
|
||||
"fieldtype": "Column Break"
|
||||
@ -1266,13 +1259,19 @@
|
||||
"fieldname": "shipping_address_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Shipping Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "connections_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Connections",
|
||||
"show_dashboard": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-05-07 20:18:09.196799",
|
||||
"modified": "2023-05-24 11:16:41.195340",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
|
@ -439,7 +439,7 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-09 15:02:44.490731",
|
||||
"modified": "2023-05-23 09:56:43.826602",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card",
|
||||
|
@ -730,7 +730,7 @@ class JobCard(Document):
|
||||
self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0]
|
||||
|
||||
if self.docstatus < 2:
|
||||
if self.for_quantity <= self.transferred_qty:
|
||||
if flt(self.for_quantity) <= flt(self.transferred_qty):
|
||||
self.status = "Material Transferred"
|
||||
|
||||
if self.time_logs:
|
||||
|
@ -451,10 +451,14 @@ frappe.ui.form.on("Material Request Plan Item", {
|
||||
for_warehouse: row.warehouse
|
||||
},
|
||||
callback: function(r) {
|
||||
let {projected_qty, actual_qty} = r.message;
|
||||
if (r.message) {
|
||||
let {projected_qty, actual_qty} = r.message[0];
|
||||
|
||||
frappe.model.set_value(cdt, cdn, 'projected_qty', projected_qty);
|
||||
frappe.model.set_value(cdt, cdn, 'actual_qty', actual_qty);
|
||||
frappe.model.set_value(cdt, cdn, {
|
||||
'projected_qty': projected_qty,
|
||||
'actual_qty': actual_qty
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -35,8 +35,12 @@
|
||||
"section_break_25",
|
||||
"prod_plan_references",
|
||||
"section_break_24",
|
||||
"get_sub_assembly_items",
|
||||
"combine_sub_items",
|
||||
"section_break_ucc4",
|
||||
"skip_available_sub_assembly_item",
|
||||
"column_break_igxl",
|
||||
"get_sub_assembly_items",
|
||||
"section_break_g4ip",
|
||||
"sub_assembly_items",
|
||||
"download_materials_request_plan_section_section",
|
||||
"download_materials_required",
|
||||
@ -351,12 +355,12 @@
|
||||
{
|
||||
"fieldname": "section_break_24",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
"hide_border": 1,
|
||||
"label": "Sub Assembly Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "sub_assembly_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Sub Assembly Items",
|
||||
"no_copy": 1,
|
||||
"options": "Production Plan Sub Assembly Item"
|
||||
},
|
||||
@ -392,13 +396,33 @@
|
||||
"fieldname": "download_materials_request_plan_section_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Download Materials Request Plan Section"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "System consider the projected quantity to check available or will be available sub-assembly items ",
|
||||
"fieldname": "skip_available_sub_assembly_item",
|
||||
"fieldtype": "Check",
|
||||
"label": "Skip Available Sub Assembly Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ucc4",
|
||||
"fieldtype": "Column Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_g4ip",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_igxl",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-calendar",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-03-31 10:30:48.118932",
|
||||
"modified": "2023-05-22 23:36:31.770517",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan",
|
||||
|
@ -718,7 +718,9 @@ class ProductionPlan(Document):
|
||||
frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx))
|
||||
|
||||
bom_data = []
|
||||
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty)
|
||||
|
||||
warehouse = row.warehouse if self.skip_available_sub_assembly_item else None
|
||||
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty, self.company, warehouse=warehouse)
|
||||
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
|
||||
sub_assembly_items_store.extend(bom_data)
|
||||
|
||||
@ -894,7 +896,9 @@ def download_raw_materials(doc, warehouses=None):
|
||||
build_csv_response(item_list, doc.name)
|
||||
|
||||
|
||||
def get_exploded_items(item_details, company, bom_no, include_non_stock_items, planned_qty=1):
|
||||
def get_exploded_items(
|
||||
item_details, company, bom_no, include_non_stock_items, planned_qty=1, doc=None
|
||||
):
|
||||
bei = frappe.qb.DocType("BOM Explosion Item")
|
||||
bom = frappe.qb.DocType("BOM")
|
||||
item = frappe.qb.DocType("Item")
|
||||
@ -1271,6 +1275,12 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
||||
include_safety_stock = doc.get("include_safety_stock")
|
||||
|
||||
so_item_details = frappe._dict()
|
||||
|
||||
sub_assembly_items = {}
|
||||
if doc.get("skip_available_sub_assembly_item"):
|
||||
for d in doc.get("sub_assembly_items"):
|
||||
sub_assembly_items.setdefault((d.get("production_item"), d.get("bom_no")), d.get("qty"))
|
||||
|
||||
for data in po_items:
|
||||
if not data.get("include_exploded_items") and doc.get("sub_assembly_items"):
|
||||
data["include_exploded_items"] = 1
|
||||
@ -1296,10 +1306,24 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
||||
frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get("idx")))
|
||||
|
||||
if bom_no:
|
||||
if data.get("include_exploded_items") and include_subcontracted_items:
|
||||
if (
|
||||
data.get("include_exploded_items")
|
||||
and doc.get("sub_assembly_items")
|
||||
and doc.get("skip_available_sub_assembly_item")
|
||||
):
|
||||
item_details = get_raw_materials_of_sub_assembly_items(
|
||||
item_details,
|
||||
company,
|
||||
bom_no,
|
||||
include_non_stock_items,
|
||||
sub_assembly_items,
|
||||
planned_qty=planned_qty,
|
||||
)
|
||||
|
||||
elif data.get("include_exploded_items") and include_subcontracted_items:
|
||||
# fetch exploded items from BOM
|
||||
item_details = get_exploded_items(
|
||||
item_details, company, bom_no, include_non_stock_items, planned_qty=planned_qty
|
||||
item_details, company, bom_no, include_non_stock_items, planned_qty=planned_qty, doc=doc
|
||||
)
|
||||
else:
|
||||
item_details = get_subitems(
|
||||
@ -1456,12 +1480,22 @@ def get_item_data(item_code):
|
||||
}
|
||||
|
||||
|
||||
def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
|
||||
def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, company, warehouse=None, indent=0):
|
||||
data = get_bom_children(parent=bom_no)
|
||||
for d in data:
|
||||
if d.expandable:
|
||||
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
|
||||
stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
|
||||
|
||||
if warehouse:
|
||||
bin_dict = get_bin_details(d, company, for_warehouse=warehouse)
|
||||
|
||||
if bin_dict and bin_dict[0].projected_qty > 0:
|
||||
if bin_dict[0].projected_qty > stock_qty:
|
||||
continue
|
||||
else:
|
||||
stock_qty = stock_qty - bin_dict[0].projected_qty
|
||||
|
||||
bom_data.append(
|
||||
frappe._dict(
|
||||
{
|
||||
@ -1481,7 +1515,7 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
|
||||
)
|
||||
|
||||
if d.value:
|
||||
get_sub_assembly_items(d.value, bom_data, stock_qty, indent=indent + 1)
|
||||
get_sub_assembly_items(d.value, bom_data, stock_qty, company, warehouse, indent=indent + 1)
|
||||
|
||||
|
||||
def set_default_warehouses(row, default_warehouses):
|
||||
@ -1519,3 +1553,68 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
|
||||
)
|
||||
|
||||
return reserved_qty_for_production_plan - reserved_qty_for_production
|
||||
|
||||
|
||||
def get_raw_materials_of_sub_assembly_items(
|
||||
item_details, company, bom_no, include_non_stock_items, sub_assembly_items, planned_qty=1
|
||||
):
|
||||
|
||||
bei = frappe.qb.DocType("BOM Item")
|
||||
bom = frappe.qb.DocType("BOM")
|
||||
item = frappe.qb.DocType("Item")
|
||||
item_default = frappe.qb.DocType("Item Default")
|
||||
item_uom = frappe.qb.DocType("UOM Conversion Detail")
|
||||
|
||||
items = (
|
||||
frappe.qb.from_(bei)
|
||||
.join(bom)
|
||||
.on(bom.name == bei.parent)
|
||||
.join(item)
|
||||
.on(item.name == bei.item_code)
|
||||
.left_join(item_default)
|
||||
.on((item_default.parent == item.name) & (item_default.company == company))
|
||||
.left_join(item_uom)
|
||||
.on((item.name == item_uom.parent) & (item_uom.uom == item.purchase_uom))
|
||||
.select(
|
||||
(IfNull(Sum(bei.stock_qty / IfNull(bom.quantity, 1)), 0) * planned_qty).as_("qty"),
|
||||
item.item_name,
|
||||
item.name.as_("item_code"),
|
||||
bei.description,
|
||||
bei.stock_uom,
|
||||
bei.bom_no,
|
||||
item.min_order_qty,
|
||||
bei.source_warehouse,
|
||||
item.default_material_request_type,
|
||||
item.min_order_qty,
|
||||
item_default.default_warehouse,
|
||||
item.purchase_uom,
|
||||
item_uom.conversion_factor,
|
||||
item.safety_stock,
|
||||
)
|
||||
.where(
|
||||
(bei.docstatus == 1)
|
||||
& (bom.name == bom_no)
|
||||
& (item.is_stock_item.isin([0, 1]) if include_non_stock_items else item.is_stock_item == 1)
|
||||
)
|
||||
.groupby(bei.item_code, bei.stock_uom)
|
||||
).run(as_dict=True)
|
||||
|
||||
for item in items:
|
||||
key = (item.item_code, item.bom_no)
|
||||
if item.bom_no and key in sub_assembly_items:
|
||||
planned_qty = flt(sub_assembly_items[key])
|
||||
get_raw_materials_of_sub_assembly_items(
|
||||
item_details,
|
||||
company,
|
||||
item.bom_no,
|
||||
include_non_stock_items,
|
||||
sub_assembly_items,
|
||||
planned_qty=planned_qty,
|
||||
)
|
||||
else:
|
||||
if not item.conversion_factor and item.purchase_uom:
|
||||
item.conversion_factor = get_uom_conversion_factor(item.item_code, item.purchase_uom)
|
||||
|
||||
item_details.setdefault(item.get("item_code"), item)
|
||||
|
||||
return item_details
|
||||
|
@ -926,6 +926,50 @@ class TestProductionPlan(FrappeTestCase):
|
||||
|
||||
self.assertEqual(after_qty, before_qty)
|
||||
|
||||
def test_skip_available_qty_for_sub_assembly_items(self):
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||
|
||||
bom_tree = {
|
||||
"Fininshed Goods1 For SUB Test": {
|
||||
"SubAssembly1 For SUB Test": {"ChildPart1 For SUB Test": {}},
|
||||
"SubAssembly2 For SUB Test": {},
|
||||
}
|
||||
}
|
||||
|
||||
parent_bom = create_nested_bom(bom_tree, prefix="")
|
||||
plan = create_production_plan(
|
||||
item_code=parent_bom.item,
|
||||
planned_qty=10,
|
||||
ignore_existing_ordered_qty=1,
|
||||
do_not_submit=1,
|
||||
skip_available_sub_assembly_item=1,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
make_stock_entry(
|
||||
item_code="SubAssembly1 For SUB Test",
|
||||
qty=5,
|
||||
rate=100,
|
||||
target="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
self.assertTrue(plan.skip_available_sub_assembly_item)
|
||||
|
||||
plan.get_sub_assembly_items()
|
||||
|
||||
for row in plan.sub_assembly_items:
|
||||
if row.production_item == "SubAssembly1 For SUB Test":
|
||||
self.assertEqual(row.qty, 5)
|
||||
|
||||
mr_items = get_items_for_material_requests(plan.as_dict())
|
||||
for row in mr_items:
|
||||
row = frappe._dict(row)
|
||||
if row.item_code == "ChildPart1 For SUB Test":
|
||||
self.assertEqual(row.quantity, 5)
|
||||
|
||||
if row.item_code == "SubAssembly2 For SUB Test":
|
||||
self.assertEqual(row.quantity, 10)
|
||||
|
||||
|
||||
def create_production_plan(**args):
|
||||
"""
|
||||
@ -945,6 +989,7 @@ def create_production_plan(**args):
|
||||
"include_subcontracted_items": args.include_subcontracted_items or 0,
|
||||
"ignore_existing_ordered_qty": args.ignore_existing_ordered_qty or 0,
|
||||
"get_items_from": "Sales Order",
|
||||
"skip_available_sub_assembly_item": args.skip_available_sub_assembly_item or 0,
|
||||
}
|
||||
)
|
||||
|
||||
@ -958,6 +1003,7 @@ def create_production_plan(**args):
|
||||
"planned_qty": args.planned_qty or 1,
|
||||
"planned_start_date": args.planned_start_date or now_datetime(),
|
||||
"stock_uom": args.stock_uom or "Nos",
|
||||
"warehouse": args.warehouse,
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -28,7 +28,11 @@
|
||||
"uom",
|
||||
"stock_uom",
|
||||
"column_break_22",
|
||||
"description"
|
||||
"description",
|
||||
"section_break_4rxf",
|
||||
"actual_qty",
|
||||
"column_break_xfhm",
|
||||
"projected_qty"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -183,12 +187,34 @@
|
||||
"fieldtype": "Datetime",
|
||||
"in_list_view": 1,
|
||||
"label": "Schedule Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_4rxf",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "actual_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Actual Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_xfhm",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "projected_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Projected Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-28 13:50:15.116082",
|
||||
"modified": "2023-05-22 17:52:34.708879",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan Sub Assembly Item",
|
||||
|
@ -2,9 +2,11 @@
|
||||
"creation": "2021-11-23 12:00:36.138824",
|
||||
"docstatus": 0,
|
||||
"doctype": "Form Tour",
|
||||
"first_document": 0,
|
||||
"idx": 0,
|
||||
"include_name_field": 0,
|
||||
"is_standard": 1,
|
||||
"modified": "2021-11-23 12:02:48.010298",
|
||||
"modified": "2023-05-23 12:51:48.684517",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Quotation",
|
||||
@ -14,51 +16,43 @@
|
||||
"steps": [
|
||||
{
|
||||
"description": "Select a customer or lead for whom this quotation is being prepared. Let's select a Customer.",
|
||||
"field": "",
|
||||
"fieldname": "quotation_to",
|
||||
"fieldtype": "Link",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Quotation To",
|
||||
"parent_field": "",
|
||||
"position": "Right",
|
||||
"title": "Quotation To"
|
||||
},
|
||||
{
|
||||
"description": "Select a specific Customer to whom this quotation will be sent.",
|
||||
"field": "",
|
||||
"fieldname": "party_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Party",
|
||||
"parent_field": "",
|
||||
"position": "Right",
|
||||
"title": "Party"
|
||||
},
|
||||
{
|
||||
"child_doctype": "Quotation Item",
|
||||
"description": "Select an item for which you will be quoting a price.",
|
||||
"field": "",
|
||||
"fieldname": "items",
|
||||
"fieldtype": "Table",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Items",
|
||||
"parent_field": "",
|
||||
"parent_fieldname": "items",
|
||||
"position": "Bottom",
|
||||
"title": "Items"
|
||||
},
|
||||
{
|
||||
"description": "You can select pre-populated Sales Taxes and Charges from here.",
|
||||
"field": "",
|
||||
"fieldname": "taxes",
|
||||
"fieldtype": "Table",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Sales Taxes and Charges",
|
||||
"parent_field": "",
|
||||
"position": "Bottom",
|
||||
"title": "Sales Taxes and Charges"
|
||||
}
|
||||
|
@ -25,7 +25,7 @@
|
||||
"documentation_url": "https://docs.erpnext.com/docs/v14/user/manual/en/setting-up/company-setup",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"modified": "2023-05-20 19:45:03.936741",
|
||||
"modified": "2023-05-23 13:20:19.703506",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Home",
|
||||
@ -38,10 +38,7 @@
|
||||
"step": "Create a Customer"
|
||||
},
|
||||
{
|
||||
"step": "Create a Supplier"
|
||||
},
|
||||
{
|
||||
"step": "Create a Quotation"
|
||||
"step": "Create Your First Sales Invoice"
|
||||
}
|
||||
],
|
||||
"subtitle": "Item, Customer, Supplier and Quotation",
|
||||
|
@ -9,7 +9,7 @@
|
||||
"is_complete": 0,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2023-05-16 20:01:34.202622",
|
||||
"modified": "2023-05-23 12:45:55.138580",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Create a Customer",
|
||||
"owner": "Administrator",
|
||||
|
@ -11,7 +11,7 @@
|
||||
"is_complete": 0,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2023-05-16 12:56:40.355878",
|
||||
"modified": "2023-05-23 12:43:08.484206",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Create an Item",
|
||||
"owner": "Administrator",
|
||||
|
@ -0,0 +1,20 @@
|
||||
{
|
||||
"action": "Create Entry",
|
||||
"creation": "2020-05-14 17:48:21.019019",
|
||||
"description": "# All about sales invoice\n\nA Sales Invoice is a bill that you send to your Customers against which the Customer makes the payment. Sales Invoice is an accounting transaction. On submission of Sales Invoice, the system updates the receivable and books income against a Customer Account.",
|
||||
"docstatus": 0,
|
||||
"doctype": "Onboarding Step",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2023-05-22 21:20:15.589644",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Create Your First Sales Invoice",
|
||||
"owner": "Administrator",
|
||||
"reference_document": "Sales Invoice",
|
||||
"show_form_tour": 1,
|
||||
"show_full_form": 1,
|
||||
"title": "Create Your First Sales Invoice ",
|
||||
"validate_action": 1
|
||||
}
|
@ -460,7 +460,7 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus)
|
||||
item_doc.qty if (docstatus == 1 and item_doc.stock_qty == 0) else item_doc.stock_qty
|
||||
)
|
||||
|
||||
while remaining_stock_qty > 0 and available_locations:
|
||||
while flt(remaining_stock_qty) > 0 and available_locations:
|
||||
item_location = available_locations.pop(0)
|
||||
item_location = frappe._dict(item_location)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user