Merge pull request #37787 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
Deepesh Garg
2023-11-01 12:10:26 +05:30
committed by GitHub
64 changed files with 966 additions and 1129 deletions

View File

@@ -30,7 +30,8 @@
{ {
"fieldname": "posting_date", "fieldname": "posting_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Posting Date" "label": "Posting Date",
"search_index": 1
}, },
{ {
"fieldname": "account_type", "fieldname": "account_type",
@@ -153,7 +154,7 @@
"in_create": 1, "in_create": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-06-29 12:24:20.500632", "modified": "2023-10-30 16:15:00.470283",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Ledger Entry", "name": "Payment Ledger Entry",

View File

@@ -776,19 +776,28 @@ class TestPOSInvoice(unittest.TestCase):
) )
create_batch_item_with_batch("_BATCH ITEM Test For Reserve", "TestBatch-RS 02") create_batch_item_with_batch("_BATCH ITEM Test For Reserve", "TestBatch-RS 02")
make_stock_entry( se = make_stock_entry(
target="_Test Warehouse - _TC", target="_Test Warehouse - _TC",
item_code="_BATCH ITEM Test For Reserve", item_code="_BATCH ITEM Test For Reserve",
qty=20, qty=30,
basic_rate=100, basic_rate=100,
batch_no="TestBatch-RS 02",
) )
se.reload()
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
# POS Invoice 1, for the batch without bundle
pos_inv1 = create_pos_invoice( pos_inv1 = create_pos_invoice(
item="_BATCH ITEM Test For Reserve", rate=300, qty=15, batch_no="TestBatch-RS 02" item="_BATCH ITEM Test For Reserve", rate=300, qty=15, do_not_save=1
) )
pos_inv1.items[0].batch_no = batch_no
pos_inv1.save() pos_inv1.save()
pos_inv1.submit() pos_inv1.submit()
pos_inv1.reload()
self.assertFalse(pos_inv1.items[0].serial_and_batch_bundle)
batches = get_auto_batch_nos( batches = get_auto_batch_nos(
frappe._dict( frappe._dict(
@@ -797,7 +806,24 @@ class TestPOSInvoice(unittest.TestCase):
) )
for batch in batches: for batch in batches:
if batch.batch_no == "TestBatch-RS 02" and batch.warehouse == "_Test Warehouse - _TC": if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC":
self.assertEqual(batch.qty, 15)
# POS Invoice 2, for the batch with bundle
pos_inv2 = create_pos_invoice(
item="_BATCH ITEM Test For Reserve", rate=300, qty=10, batch_no=batch_no
)
pos_inv2.reload()
self.assertTrue(pos_inv2.items[0].serial_and_batch_bundle)
batches = get_auto_batch_nos(
frappe._dict(
{"item_code": "_BATCH ITEM Test For Reserve", "warehouse": "_Test Warehouse - _TC"}
)
)
for batch in batches:
if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC":
self.assertEqual(batch.qty, 5) self.assertEqual(batch.qty, 5)
def test_pos_batch_item_qty_validation(self): def test_pos_batch_item_qty_validation(self):

View File

@@ -1837,6 +1837,7 @@ def make_purchase_receipt(source_name, target_doc=None):
"po_detail": "purchase_order_item", "po_detail": "purchase_order_item",
"material_request": "material_request", "material_request": "material_request",
"material_request_item": "material_request_item", "material_request_item": "material_request_item",
"wip_composite_asset": "wip_composite_asset",
}, },
"postprocess": update_item, "postprocess": update_item,
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty), "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty),

View File

@@ -2553,6 +2553,7 @@ class TestSalesInvoice(FrappeTestCase):
) )
si = frappe.copy_doc(test_records[0]) si = frappe.copy_doc(test_records[0])
si.customer = "_Test Internal Customer 3"
si.update_stock = 1 si.update_stock = 1
si.set_warehouse = "Finished Goods - _TC" si.set_warehouse = "Finished Goods - _TC"
si.set_target_warehouse = "Stores - _TC" si.set_target_warehouse = "Stores - _TC"
@@ -3696,6 +3697,20 @@ def create_internal_parties():
allowed_to_interact_with="_Test Company with perpetual inventory", allowed_to_interact_with="_Test Company with perpetual inventory",
) )
create_internal_customer(
customer_name="_Test Internal Customer 3",
represents_company="_Test Company",
allowed_to_interact_with="_Test Company",
)
account = create_account(
account_name="Unrealized Profit",
parent_account="Current Liabilities - _TC",
company="_Test Company",
)
frappe.db.set_value("Company", "_Test Company", "unrealized_profit_loss_account", account)
create_internal_supplier( create_internal_supplier(
supplier_name="_Test Internal Supplier", supplier_name="_Test Internal Supplier",
represents_company="Wind Power LLC", represents_company="Wind Power LLC",

View File

@@ -5,7 +5,7 @@
from typing import Optional from typing import Optional
import frappe import frappe
from frappe import _, msgprint, scrub from frappe import _, msgprint, qb, scrub
from frappe.contacts.doctype.address.address import get_company_address, get_default_address from frappe.contacts.doctype.address.address import get_company_address, get_default_address
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
from frappe.model.utils import get_fetch_values from frappe.model.utils import get_fetch_values
@@ -480,11 +480,19 @@ def get_party_account_currency(party_type, party, company):
def get_party_gle_currency(party_type, party, company): def get_party_gle_currency(party_type, party, company):
def generator(): def generator():
existing_gle_currency = frappe.db.sql( gl = qb.DocType("GL Entry")
"""select account_currency from `tabGL Entry` existing_gle_currency = (
where docstatus=1 and company=%(company)s and party_type=%(party_type)s and party=%(party)s qb.from_(gl)
limit 1""", .select(gl.account_currency)
{"company": company, "party_type": party_type, "party": party}, .where(
(gl.docstatus == 1)
& (gl.company == company)
& (gl.party_type == party_type)
& (gl.party == party)
& (gl.is_cancelled == 0)
)
.limit(1)
.run()
) )
return existing_gle_currency[0][0] if existing_gle_currency else None return existing_gle_currency[0][0] if existing_gle_currency else None

View File

@@ -718,6 +718,7 @@ class ReceivablePayableReport(object):
query = ( query = (
qb.from_(ple) qb.from_(ple)
.select( .select(
ple.name,
ple.account, ple.account,
ple.voucher_type, ple.voucher_type,
ple.voucher_no, ple.voucher_no,
@@ -731,13 +732,15 @@ class ReceivablePayableReport(object):
ple.account_currency, ple.account_currency,
ple.amount, ple.amount,
ple.amount_in_account_currency, ple.amount_in_account_currency,
ple.remarks,
) )
.where(ple.delinked == 0) .where(ple.delinked == 0)
.where(Criterion.all(self.qb_selection_filter)) .where(Criterion.all(self.qb_selection_filter))
.where(Criterion.any(self.or_filters)) .where(Criterion.any(self.or_filters))
) )
if self.filters.get("show_remarks"):
query = query.select(ple.remarks)
if self.filters.get("group_by_party"): if self.filters.get("group_by_party"):
query = query.orderby(self.ple.party, self.ple.posting_date) query = query.orderby(self.ple.party, self.ple.posting_date)
else: else:

View File

@@ -32,13 +32,6 @@ frappe.query_reports["Profitability Analysis"] = {
"label": __("Accounting Dimension"), "label": __("Accounting Dimension"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Accounting Dimension", "options": "Accounting Dimension",
"get_query": () =>{
return {
filters: {
"disabled": 0
}
}
}
}, },
{ {
"fieldname": "fiscal_year", "fieldname": "fiscal_year",

View File

@@ -9,7 +9,6 @@ frappe.ui.form.on('Asset', {
frm.set_query("item_code", function() { frm.set_query("item_code", function() {
return { return {
"filters": { "filters": {
"disabled": 0,
"is_fixed_asset": 1, "is_fixed_asset": 1,
"is_stock_item": 0 "is_stock_item": 0
} }

View File

@@ -221,11 +221,11 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:!(doc.is_composite_asset && !doc.capitalized_in)",
"fieldname": "gross_purchase_amount", "fieldname": "gross_purchase_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Gross Purchase Amount", "label": "Gross Purchase Amount",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1,
"read_only_depends_on": "eval:!doc.is_existing_asset", "read_only_depends_on": "eval:!doc.is_existing_asset",
"reqd": 1 "reqd": 1
}, },
@@ -399,6 +399,7 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"depends_on": "eval:!doc.is_composite_asset && !doc.is_existing_asset",
"fieldname": "purchase_receipt", "fieldname": "purchase_receipt",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Purchase Receipt", "label": "Purchase Receipt",
@@ -416,6 +417,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:!doc.is_composite_asset && !doc.is_existing_asset",
"fieldname": "purchase_invoice", "fieldname": "purchase_invoice",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Purchase Invoice", "label": "Purchase Invoice",
@@ -479,10 +481,11 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval.doc.asset_quantity",
"fieldname": "asset_quantity", "fieldname": "asset_quantity",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Asset Quantity", "label": "Asset Quantity",
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset" "read_only": 1
}, },
{ {
"fieldname": "depr_entry_posting_status", "fieldname": "depr_entry_posting_status",
@@ -562,9 +565,14 @@
"link_doctype": "Journal Entry", "link_doctype": "Journal Entry",
"link_fieldname": "reference_name", "link_fieldname": "reference_name",
"table_fieldname": "accounts" "table_fieldname": "accounts"
},
{
"group": "Asset Capitalization",
"link_doctype": "Asset Capitalization",
"link_fieldname": "target_asset"
} }
], ],
"modified": "2023-10-03 23:28:26.732269", "modified": "2023-10-27 17:03:46.629617",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset", "name": "Asset",
@@ -608,4 +616,4 @@
"states": [], "states": [],
"title_field": "asset_name", "title_field": "asset_name",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -876,12 +876,8 @@ def get_items_tagged_to_wip_composite_asset(asset):
"amount", "amount",
] ]
pi_items = frappe.get_all(
"Purchase Invoice Item", filters={"wip_composite_asset": asset}, fields=fields
)
pr_items = frappe.get_all( pr_items = frappe.get_all(
"Purchase Receipt Item", filters={"wip_composite_asset": asset}, fields=fields "Purchase Receipt Item", filters={"wip_composite_asset": asset}, fields=fields
) )
return pi_items + pr_items return pr_items

View File

@@ -16,7 +16,7 @@
"transaction_settings_section", "transaction_settings_section",
"po_required", "po_required",
"pr_required", "pr_required",
"over_order_allowance", "blanket_order_allowance",
"column_break_12", "column_break_12",
"maintain_same_rate", "maintain_same_rate",
"set_landed_cost_based_on_purchase_invoice_rate", "set_landed_cost_based_on_purchase_invoice_rate",
@@ -159,19 +159,19 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Set Landed Cost Based on Purchase Invoice Rate" "label": "Set Landed Cost Based on Purchase Invoice Rate"
}, },
{
"default": "0",
"description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.",
"fieldname": "over_order_allowance",
"fieldtype": "Float",
"label": "Over Order Allowance (%)"
},
{ {
"default": "0", "default": "0",
"description": "While making Purchase Invoice from Purchase Order, use Exchange Rate on Invoice's transaction date rather than inheriting it from Purchase Order. Only applies for Purchase Invoice.", "description": "While making Purchase Invoice from Purchase Order, use Exchange Rate on Invoice's transaction date rather than inheriting it from Purchase Order. Only applies for Purchase Invoice.",
"fieldname": "use_transaction_date_exchange_rate", "fieldname": "use_transaction_date_exchange_rate",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Use Transaction Date Exchange Rate" "label": "Use Transaction Date Exchange Rate"
},
{
"default": "0",
"description": "Percentage you are allowed to order beyond the Blanket Order quantity.",
"fieldname": "blanket_order_allowance",
"fieldtype": "Float",
"label": "Blanket Order Allowance (%)"
} }
], ],
"icon": "fa fa-cog", "icon": "fa fa-cog",
@@ -179,7 +179,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-10-16 16:22:03.201078", "modified": "2023-10-25 14:03:32.520418",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying Settings", "name": "Buying Settings",

View File

@@ -556,6 +556,9 @@ def make_purchase_receipt(source_name, target_doc=None):
"bom": "bom", "bom": "bom",
"material_request": "material_request", "material_request": "material_request",
"material_request_item": "material_request_item", "material_request_item": "material_request_item",
"sales_order": "sales_order",
"sales_order_item": "sales_order_item",
"wip_composite_asset": "wip_composite_asset",
}, },
"postprocess": update_item, "postprocess": update_item,
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty)
@@ -632,6 +635,7 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
"field_map": { "field_map": {
"name": "po_detail", "name": "po_detail",
"parent": "purchase_order", "parent": "purchase_order",
"wip_composite_asset": "wip_composite_asset",
}, },
"postprocess": update_item, "postprocess": update_item,
"condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)), "condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)),

View File

@@ -86,6 +86,8 @@
"billed_amt", "billed_amt",
"accounting_details", "accounting_details",
"expense_account", "expense_account",
"column_break_fyqr",
"wip_composite_asset",
"manufacture_details", "manufacture_details",
"manufacturer", "manufacturer",
"manufacturer_part_no", "manufacturer_part_no",
@@ -896,13 +898,23 @@
"fieldname": "apply_tds", "fieldname": "apply_tds",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Apply TDS" "label": "Apply TDS"
},
{
"fieldname": "wip_composite_asset",
"fieldtype": "Link",
"label": "WIP Composite Asset",
"options": "Asset"
},
{
"fieldname": "column_break_fyqr",
"fieldtype": "Column Break"
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-09-13 16:22:40.825092", "modified": "2023-10-27 15:50:42.655573",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order Item", "name": "Purchase Order Item",
@@ -915,4 +927,4 @@
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -44,11 +44,6 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
} }
} }
} }
else {
return {
filters: { "disabled": 0 }
}
}
} }
}, },
{ {

View File

@@ -693,13 +693,21 @@ class StockController(AccountsController):
d.stock_uom_rate = d.rate / (d.conversion_factor or 1) d.stock_uom_rate = d.rate / (d.conversion_factor or 1)
def validate_internal_transfer(self): def validate_internal_transfer(self):
if ( if self.doctype in ("Sales Invoice", "Delivery Note", "Purchase Invoice", "Purchase Receipt"):
self.doctype in ("Sales Invoice", "Delivery Note", "Purchase Invoice", "Purchase Receipt") if self.is_internal_transfer():
and self.is_internal_transfer() self.validate_in_transit_warehouses()
): self.validate_multi_currency()
self.validate_in_transit_warehouses() self.validate_packed_items()
self.validate_multi_currency() else:
self.validate_packed_items() self.validate_internal_transfer_warehouse()
def validate_internal_transfer_warehouse(self):
for row in self.items:
if row.get("target_warehouse"):
row.target_warehouse = None
if row.get("from_warehouse"):
row.from_warehouse = None
def validate_in_transit_warehouses(self): def validate_in_transit_warehouses(self):
if ( if (

View File

@@ -107,7 +107,7 @@ def validate_against_blanket_order(order_doc):
allowance = flt( allowance = flt(
frappe.db.get_single_value( frappe.db.get_single_value(
"Selling Settings" if order_doc.doctype == "Sales Order" else "Buying Settings", "Selling Settings" if order_doc.doctype == "Sales Order" else "Buying Settings",
"over_order_allowance", "blanket_order_allowance",
) )
) )
for bo_name, item_data in order_data.items(): for bo_name, item_data in order_data.items():

View File

@@ -63,7 +63,7 @@ class TestBlanketOrder(FrappeTestCase):
po1.currency = get_company_currency(po1.company) po1.currency = get_company_currency(po1.company)
self.assertEqual(po1.items[0].qty, (bo.items[0].qty - bo.items[0].ordered_qty)) self.assertEqual(po1.items[0].qty, (bo.items[0].qty - bo.items[0].ordered_qty))
def test_over_order_allowance(self): def test_blanket_order_allowance(self):
# Sales Order # Sales Order
bo = make_blanket_order(blanket_order_type="Selling", quantity=100) bo = make_blanket_order(blanket_order_type="Selling", quantity=100)
@@ -74,7 +74,7 @@ class TestBlanketOrder(FrappeTestCase):
so.items[0].qty = 110 so.items[0].qty = 110
self.assertRaises(frappe.ValidationError, so.submit) self.assertRaises(frappe.ValidationError, so.submit)
frappe.db.set_single_value("Selling Settings", "over_order_allowance", 10) frappe.db.set_single_value("Selling Settings", "blanket_order_allowance", 10)
so.submit() so.submit()
# Purchase Order # Purchase Order
@@ -87,7 +87,7 @@ class TestBlanketOrder(FrappeTestCase):
po.items[0].qty = 110 po.items[0].qty = 110
self.assertRaises(frappe.ValidationError, po.submit) self.assertRaises(frappe.ValidationError, po.submit)
frappe.db.set_single_value("Buying Settings", "over_order_allowance", 10) frappe.db.set_single_value("Buying Settings", "blanket_order_allowance", 10)
po.submit() po.submit()

View File

@@ -10,8 +10,8 @@ frappe.views.calendar["Job Card"] = {
}, },
gantt: { gantt: {
field_map: { field_map: {
"start": "started_time", "start": "expected_start_date",
"end": "started_time", "end": "expected_end_date",
"id": "name", "id": "name",
"title": "subject", "title": "subject",
"color": "color", "color": "color",

View File

@@ -1,6 +1,6 @@
frappe.listview_settings['Job Card'] = { frappe.listview_settings['Job Card'] = {
has_indicator_for_draft: true, has_indicator_for_draft: true,
add_fields: ["expected_start_date", "expected_end_date"],
get_indicator: function(doc) { get_indicator: function(doc) {
const status_colors = { const status_colors = {
"Work In Progress": "orange", "Work In Progress": "orange",

View File

@@ -1735,7 +1735,10 @@ def get_raw_materials_of_sub_assembly_items(
if not item.conversion_factor and item.purchase_uom: if not item.conversion_factor and item.purchase_uom:
item.conversion_factor = get_uom_conversion_factor(item.item_code, item.purchase_uom) item.conversion_factor = get_uom_conversion_factor(item.item_code, item.purchase_uom)
item_details.setdefault(item.get("item_code"), item) if details := item_details.get(item.get("item_code")):
details.qty += item.get("qty")
else:
item_details.setdefault(item.get("item_code"), item)
return item_details return item_details

View File

@@ -1332,6 +1332,33 @@ class TestProductionPlan(FrappeTestCase):
self.assertTrue(row.warehouse == mrp_warhouse) self.assertTrue(row.warehouse == mrp_warhouse)
self.assertEqual(row.quantity, 12) self.assertEqual(row.quantity, 12)
def test_mr_qty_for_same_rm_with_different_sub_assemblies(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
bom_tree = {
"Fininshed Goods2 For SUB Test": {
"SubAssembly2 For SUB Test": {"ChildPart2 For SUB Test": {}},
"SubAssembly3 For SUB Test": {"ChildPart2 For SUB Test": {}},
}
}
parent_bom = create_nested_bom(bom_tree, prefix="")
plan = create_production_plan(
item_code=parent_bom.item,
planned_qty=1,
ignore_existing_ordered_qty=1,
do_not_submit=1,
skip_available_sub_assembly_item=1,
warehouse="_Test Warehouse - _TC",
)
plan.get_sub_assembly_items()
plan.make_material_request()
for row in plan.mr_items:
if row.item_code == "ChildPart2 For SUB Test":
self.assertEqual(row.quantity, 2)
def create_production_plan(**args): def create_production_plan(**args):
""" """

View File

@@ -12,7 +12,7 @@ frappe.query_reports["BOM Operations Time"] = {
"options": "Item", "options": "Item",
"get_query": () =>{ "get_query": () =>{
return { return {
filters: { "disabled": 0, "is_stock_item": 1 } filters: { "is_stock_item": 1 }
} }
} }
}, },

View File

@@ -316,7 +316,7 @@ erpnext.patches.v14_0.update_closing_balances #14-07-2023
execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0) execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
erpnext.patches.v14_0.update_reference_type_in_journal_entry_accounts erpnext.patches.v14_0.update_reference_type_in_journal_entry_accounts
erpnext.patches.v14_0.update_subscription_details erpnext.patches.v14_0.update_subscription_details
execute:frappe.delete_doc_if_exists("Report", "Tax Detail") execute:frappe.delete_doc("Report", "Tax Detail", force=True)
erpnext.patches.v15_0.enable_all_leads erpnext.patches.v15_0.enable_all_leads
erpnext.patches.v14_0.update_company_in_ldc erpnext.patches.v14_0.update_company_in_ldc
erpnext.patches.v14_0.set_packed_qty_in_draft_delivery_notes erpnext.patches.v14_0.set_packed_qty_in_draft_delivery_notes
@@ -340,5 +340,10 @@ erpnext.patches.v14_0.update_invoicing_period_in_subscription
execute:frappe.delete_doc("Page", "welcome-to-erpnext") execute:frappe.delete_doc("Page", "welcome-to-erpnext")
erpnext.patches.v15_0.delete_payment_gateway_doctypes erpnext.patches.v15_0.delete_payment_gateway_doctypes
erpnext.patches.v14_0.create_accounting_dimensions_in_sales_order_item erpnext.patches.v14_0.create_accounting_dimensions_in_sales_order_item
erpnext.patches.v15_0.update_sre_from_voucher_details
erpnext.patches.v14_0.rename_over_order_allowance_field
erpnext.patches.v14_0.migrate_delivery_stop_lock_field
execute:frappe.db.set_single_value("Payment Reconciliation", "invoice_limit", 50)
execute:frappe.db.set_single_value("Payment Reconciliation", "payment_limit", 50)
# below migration patch should always run last # below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.patches.v14_0.migrate_gl_to_payment_ledger

View File

@@ -0,0 +1,7 @@
import frappe
from frappe.model.utils.rename_field import rename_field
def execute():
if frappe.db.has_column("Delivery Stop", "lock"):
rename_field("Delivery Stop", "lock", "locked")

View File

@@ -0,0 +1,15 @@
from frappe.model.utils.rename_field import rename_field
def execute():
rename_field(
"Buying Settings",
"over_order_allowance",
"blanket_order_allowance",
)
rename_field(
"Selling Settings",
"over_order_allowance",
"blanket_order_allowance",
)

View File

@@ -0,0 +1,18 @@
import frappe
from frappe.query_builder.functions import IfNull
def execute():
columns = frappe.db.get_table_columns("Stock Reservation Entry")
if set(["against_pick_list", "against_pick_list_item"]).issubset(set(columns)):
sre = frappe.qb.DocType("Stock Reservation Entry")
(
frappe.qb.update(sre)
.set(sre.from_voucher_type, "Pick List")
.set(sre.from_voucher_no, sre.against_pick_list)
.set(sre.from_voucher_detail_no, sre.against_pick_list_item)
.where(
(IfNull(sre.against_pick_list, "") != "") & (IfNull(sre.against_pick_list_item, "") != "")
)
).run()

View File

@@ -30,7 +30,6 @@ erpnext.accounts.taxes = {
filters: { filters: {
"account_type": account_type, "account_type": account_type,
"company": doc.company, "company": doc.company,
"disabled": 0
} }
} }
}); });

View File

@@ -87,17 +87,13 @@ frappe.ui.form.on("Sales Order", {
frm.events.get_items_from_internal_purchase_order(frm); frm.events.get_items_from_internal_purchase_order(frm);
} }
if (frm.is_new()) { if (frm.doc.docstatus === 0) {
frappe.db.get_single_value("Stock Settings", "enable_stock_reservation").then((value) => { frappe.db.get_single_value("Stock Settings", "enable_stock_reservation").then((value) => {
if (value) { if (!value) {
frappe.db.get_single_value("Stock Settings", "auto_reserve_stock_for_sales_order").then((value) => { // If `Stock Reservation` is disabled in Stock Settings, set Reserve Stock to 0 and make the field read-only and hidden.
// If `Reserve Stock on Sales Order Submission` is enabled in Stock Settings, set Reserve Stock to 1 else 0.
frm.set_value("reserve_stock", value ? 1 : 0);
})
} else {
// If `Stock Reservation` is disabled in Stock Settings, set Reserve Stock to 0 and read only.
frm.set_value("reserve_stock", 0); frm.set_value("reserve_stock", 0);
frm.set_df_property("reserve_stock", "read_only", 1); frm.set_df_property("reserve_stock", "read_only", 1);
frm.set_df_property("reserve_stock", "hidden", 1);
} }
}) })
} }

View File

@@ -1631,10 +1631,9 @@
{ {
"default": "0", "default": "0",
"depends_on": "eval: (doc.docstatus == 0 || doc.reserve_stock)", "depends_on": "eval: (doc.docstatus == 0 || doc.reserve_stock)",
"description": "If checked, Stock Reservation Entries will be created on <b>Submit</b>", "description": "If checked, Stock will be reserved on <b>Submit</b>",
"fieldname": "reserve_stock", "fieldname": "reserve_stock",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 1,
"label": "Reserve Stock", "label": "Reserve Stock",
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
@@ -1645,7 +1644,7 @@
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-07-24 08:59:11.599875", "modified": "2023-10-18 12:41:54.813462",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order", "name": "Sales Order",

View File

@@ -3,6 +3,7 @@
import json import json
from typing import Literal
import frappe import frappe
import frappe.utils import frappe.utils
@@ -534,14 +535,24 @@ class SalesOrder(SellingController):
return False return False
@frappe.whitelist() @frappe.whitelist()
def create_stock_reservation_entries(self, items_details=None, notify=True) -> None: def create_stock_reservation_entries(
self,
items_details: list[dict] = None,
from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None,
notify=True,
) -> None:
"""Creates Stock Reservation Entries for Sales Order Items.""" """Creates Stock Reservation Entries for Sales Order Items."""
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
create_stock_reservation_entries_for_so_items as create_stock_reservation_entries, create_stock_reservation_entries_for_so_items as create_stock_reservation_entries,
) )
create_stock_reservation_entries(so=self, items_details=items_details, notify=notify) create_stock_reservation_entries(
sales_order=self,
items_details=items_details,
from_voucher_type=from_voucher_type,
notify=notify,
)
@frappe.whitelist() @frappe.whitelist()
def cancel_stock_reservation_entries(self, sre_list=None, notify=True) -> None: def cancel_stock_reservation_entries(self, sre_list=None, notify=True) -> None:
@@ -748,6 +759,8 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None):
if target.company_address: if target.company_address:
target.update(get_fetch_values("Delivery Note", "company_address", target.company_address)) target.update(get_fetch_values("Delivery Note", "company_address", target.company_address))
# set target items names to ensure proper linking with packed_items
target.set_new_name()
make_packing_list(target) make_packing_list(target)
def condition(doc): def condition(doc):
@@ -820,6 +833,7 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None):
"postprocess": update_dn_item, "postprocess": update_dn_item,
} }
}, },
ignore_permissions=True,
) )
dn_item.qty = flt(sre.reserved_qty) * flt(dn_item.get("conversion_factor", 1)) dn_item.qty = flt(sre.reserved_qty) * flt(dn_item.get("conversion_factor", 1))

View File

@@ -25,7 +25,7 @@
"so_required", "so_required",
"dn_required", "dn_required",
"sales_update_frequency", "sales_update_frequency",
"over_order_allowance", "blanket_order_allowance",
"column_break_5", "column_break_5",
"allow_multiple_items", "allow_multiple_items",
"allow_against_multiple_purchase_orders", "allow_against_multiple_purchase_orders",
@@ -183,12 +183,6 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow Sales Order Creation For Expired Quotation" "label": "Allow Sales Order Creation For Expired Quotation"
}, },
{
"description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.",
"fieldname": "over_order_allowance",
"fieldtype": "Float",
"label": "Over Order Allowance (%)"
},
{ {
"default": "0", "default": "0",
"fieldname": "dont_reserve_sales_order_qty_on_sales_return", "fieldname": "dont_reserve_sales_order_qty_on_sales_return",
@@ -200,6 +194,12 @@
"fieldname": "allow_negative_rates_for_items", "fieldname": "allow_negative_rates_for_items",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow Negative rates for Items" "label": "Allow Negative rates for Items"
},
{
"description": "Percentage you are allowed to sell beyond the Blanket Order quantity.",
"fieldname": "blanket_order_allowance",
"fieldtype": "Float",
"label": "Blanket Order Allowance (%)"
} }
], ],
"icon": "fa fa-cog", "icon": "fa fa-cog",
@@ -207,7 +207,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-08-14 20:33:05.693667", "modified": "2023-10-25 14:03:03.966701",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Selling Settings", "name": "Selling Settings",

View File

@@ -40,7 +40,7 @@ frappe.ui.form.on("Company", {
filters:{ filters:{
'warehouse_type' : 'Transit', 'warehouse_type' : 'Transit',
'is_group': 0, 'is_group': 0,
'company': frm.doc.company 'company': frm.doc.company_name
} }
}; };
}); });

View File

@@ -1,6 +1,6 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
import gzip
import json import json
import frappe import frappe
@@ -8,7 +8,7 @@ from frappe import _
from frappe.core.doctype.prepared_report.prepared_report import create_json_gz_file from frappe.core.doctype.prepared_report.prepared_report import create_json_gz_file
from frappe.desk.form.load import get_attachments from frappe.desk.form.load import get_attachments
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import get_link_to_form, gzip_decompress, parse_json from frappe.utils import get_link_to_form, parse_json
from frappe.utils.background_jobs import enqueue from frappe.utils.background_jobs import enqueue
from erpnext.stock.report.stock_balance.stock_balance import execute from erpnext.stock.report.stock_balance.stock_balance import execute
@@ -109,7 +109,7 @@ class ClosingStockBalance(Document):
attachment = attachments[0] attachment = attachments[0]
attached_file = frappe.get_doc("File", attachment.name) attached_file = frappe.get_doc("File", attachment.name)
data = gzip_decompress(attached_file.get_content()) data = gzip.decompress(attached_file.get_content())
if data := json.loads(data.decode("utf-8")): if data := json.loads(data.decode("utf-8")):
data = data data = data

View File

@@ -1230,6 +1230,21 @@ class TestDeliveryNote(FrappeTestCase):
frappe.db.rollback() frappe.db.rollback()
frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0) frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0)
def test_non_internal_transfer_delivery_note(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
dn = create_delivery_note(do_not_submit=True)
warehouse = create_warehouse("Internal Transfer Warehouse", company=dn.company)
dn.items[0].db_set("target_warehouse", warehouse)
dn.reload()
self.assertEqual(dn.items[0].target_warehouse, warehouse)
dn.save()
dn.reload()
self.assertFalse(dn.items[0].target_warehouse)
def create_delivery_note(**args): def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note") dn = frappe.new_doc("Delivery Note")

View File

@@ -1,815 +1,197 @@
{ {
"allow_copy": 0, "actions": [],
"allow_events_in_timeline": 0, "creation": "2017-10-16 16:46:28.166950",
"allow_guest_to_view": 0, "doctype": "DocType",
"allow_import": 0, "editable_grid": 1,
"allow_rename": 0, "engine": "InnoDB",
"beta": 0, "field_order": [
"creation": "2017-10-16 16:46:28.166950", "customer",
"custom": 0, "address",
"docstatus": 0, "locked",
"doctype": "DocType", "column_break_6",
"document_type": "", "customer_address",
"editable_grid": 1, "visited",
"engine": "InnoDB", "order_information_section",
"delivery_note",
"cb_order",
"grand_total",
"section_break_7",
"contact",
"email_sent_to",
"column_break_7",
"customer_contact",
"section_break_9",
"distance",
"estimated_arrival",
"lat",
"column_break_19",
"uom",
"lng",
"more_information_section",
"details"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "columns": 2,
"allow_in_quick_entry": 0, "fieldname": "customer",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Customer",
"columns": 2, "options": "Customer"
"fieldname": "customer", },
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Customer",
"length": 0,
"no_copy": 0,
"options": "Customer",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "address",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Address Name",
"collapsible": 0, "options": "Address",
"columns": 0, "print_hide": 1,
"fieldname": "address", "reqd": 1
"fieldtype": "Link", },
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Address Name",
"length": 0,
"no_copy": 0,
"options": "Address",
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0, "fieldname": "locked",
"allow_on_submit": 0, "fieldtype": "Check",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Locked"
"columns": 0, },
"fieldname": "lock",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Lock",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_6",
"allow_in_quick_entry": 0, "fieldtype": "Column Break"
"allow_on_submit": 0, },
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_6",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "customer_address",
"allow_in_quick_entry": 0, "fieldtype": "Small Text",
"allow_on_submit": 0, "label": "Customer Address",
"bold": 0, "read_only": 1
"collapsible": 0, },
"columns": 0,
"fieldname": "customer_address",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Customer Address",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "allow_on_submit": 1,
"allow_in_quick_entry": 0, "default": "0",
"allow_on_submit": 1, "depends_on": "eval:doc.docstatus==1",
"bold": 0, "fieldname": "visited",
"collapsible": 0, "fieldtype": "Check",
"columns": 0, "label": "Visited",
"depends_on": "eval:doc.docstatus==1", "no_copy": 1,
"fieldname": "visited", "print_hide": 1
"fieldtype": "Check", },
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Visited",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "order_information_section",
"allow_in_quick_entry": 0, "fieldtype": "Section Break",
"allow_on_submit": 0, "label": "Order Information"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "order_information_section",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Order Information",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "delivery_note",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Delivery Note",
"collapsible": 0, "no_copy": 1,
"columns": 0, "options": "Delivery Note",
"fieldname": "delivery_note", "print_hide": 1,
"fieldtype": "Link", "read_only": 1
"hidden": 0, },
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Delivery Note",
"length": 0,
"no_copy": 1,
"options": "Delivery Note",
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "cb_order",
"allow_in_quick_entry": 0, "fieldtype": "Column Break"
"allow_on_submit": 0, },
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "cb_order",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "grand_total",
"allow_in_quick_entry": 0, "fieldtype": "Currency",
"allow_on_submit": 0, "label": "Grand Total",
"bold": 0, "read_only": 1
"collapsible": 0, },
"columns": 0,
"fieldname": "grand_total",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Grand Total",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "section_break_7",
"allow_in_quick_entry": 0, "fieldtype": "Section Break",
"allow_on_submit": 0, "label": "Contact Information"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Contact Information",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "contact",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "label": "Contact Name",
"bold": 0, "options": "Contact",
"collapsible": 0, "print_hide": 1
"columns": 0, },
"fieldname": "contact",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Contact Name",
"length": 0,
"no_copy": 0,
"options": "Contact",
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "email_sent_to",
"allow_in_quick_entry": 0, "fieldtype": "Data",
"allow_on_submit": 0, "label": "Email sent to",
"bold": 0, "read_only": 1
"collapsible": 0, },
"columns": 0,
"fieldname": "email_sent_to",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Email sent to",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_7",
"allow_in_quick_entry": 0, "fieldtype": "Column Break"
"allow_on_submit": 0, },
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_7",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "customer_contact",
"allow_in_quick_entry": 0, "fieldtype": "Small Text",
"allow_on_submit": 0, "label": "Customer Contact",
"bold": 0, "read_only": 1
"collapsible": 0, },
"columns": 0,
"fieldname": "customer_contact",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Customer Contact",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "section_break_9",
"allow_in_quick_entry": 0, "fieldtype": "Section Break",
"allow_on_submit": 0, "label": "Dispatch Information"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_9",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Dispatch Information",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "distance",
"allow_in_quick_entry": 0, "fieldtype": "Float",
"allow_on_submit": 0, "label": "Distance",
"bold": 0, "precision": "2",
"collapsible": 0, "read_only": 1
"columns": 0, },
"fieldname": "distance",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Distance",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "2",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "estimated_arrival",
"allow_in_quick_entry": 0, "fieldtype": "Datetime",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Estimated Arrival"
"collapsible": 0, },
"columns": 0,
"fieldname": "estimated_arrival",
"fieldtype": "Datetime",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Estimated Arrival",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "lat",
"allow_in_quick_entry": 0, "fieldtype": "Float",
"allow_on_submit": 0, "hidden": 1,
"bold": 0, "label": "Latitude"
"collapsible": 0, },
"columns": 0,
"fieldname": "lat",
"fieldtype": "Float",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Latitude",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_19",
"allow_in_quick_entry": 0, "fieldtype": "Column Break"
"allow_on_submit": 0, },
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_19",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "depends_on": "eval:doc.distance",
"allow_in_quick_entry": 0, "fieldname": "uom",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "label": "UOM",
"collapsible": 0, "options": "UOM",
"columns": 0, "read_only": 1
"default": "", },
"depends_on": "eval:doc.distance",
"fieldname": "uom",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "UOM",
"length": 0,
"no_copy": 0,
"options": "UOM",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "lng",
"allow_in_quick_entry": 0, "fieldtype": "Float",
"allow_on_submit": 0, "hidden": 1,
"bold": 0, "label": "Longitude"
"collapsible": 0, },
"columns": 0,
"fieldname": "lng",
"fieldtype": "Float",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Longitude",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "more_information_section",
"allow_in_quick_entry": 0, "fieldtype": "Section Break",
"allow_on_submit": 0, "label": "More Information"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "more_information_section",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "More Information",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "details",
"allow_in_quick_entry": 0, "fieldtype": "Text Editor",
"allow_on_submit": 0, "label": "Details"
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "details",
"fieldtype": "Text Editor",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Details",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0, "istable": 1,
"hide_heading": 0, "links": [],
"hide_toolbar": 0, "modified": "2023-09-29 09:22:53.435161",
"idx": 0, "modified_by": "Administrator",
"image_view": 0, "module": "Stock",
"in_create": 0, "name": "Delivery Stop",
"is_submittable": 0, "owner": "Administrator",
"issingle": 0, "permissions": [],
"istable": 1, "quick_entry": 1,
"max_attachments": 0, "sort_field": "modified",
"modified": "2018-10-16 05:23:25.661542", "sort_order": "DESC",
"modified_by": "Administrator", "states": [],
"module": "Stock", "track_changes": 1
"name": "Delivery Stop",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
} }

View File

@@ -64,6 +64,11 @@ frappe.ui.form.on('Delivery Trip', {
}) })
}, __("Get stops from")); }, __("Get stops from"));
} }
frm.add_custom_button(__("Delivery Notes"), function () {
frappe.set_route("List", "Delivery Note",
{'name': ["in", frm.doc.delivery_stops.map((stop) => {return stop.delivery_note;})]}
);
}, __("View"));
}, },
calculate_arrival_time: function (frm) { calculate_arrival_time: function (frm) {

View File

@@ -170,7 +170,7 @@ class DeliveryTrip(Document):
for stop in self.delivery_stops: for stop in self.delivery_stops:
leg.append(stop.customer_address) leg.append(stop.customer_address)
if optimize and stop.lock: if optimize and stop.locked:
route_list.append(leg) route_list.append(leg)
leg = [stop.customer_address] leg = [stop.customer_address]

View File

@@ -46,7 +46,7 @@ class TestDeliveryTrip(FrappeTestCase):
self.assertEqual(len(route_list[0]), 4) self.assertEqual(len(route_list[0]), 4)
def test_unoptimized_route_list_with_locks(self): def test_unoptimized_route_list_with_locks(self):
self.delivery_trip.delivery_stops[0].lock = 1 self.delivery_trip.delivery_stops[0].locked = 1
self.delivery_trip.save() self.delivery_trip.save()
route_list = self.delivery_trip.form_route_list(optimize=False) route_list = self.delivery_trip.form_route_list(optimize=False)
@@ -65,7 +65,7 @@ class TestDeliveryTrip(FrappeTestCase):
self.assertEqual(len(route_list[0]), 4) self.assertEqual(len(route_list[0]), 4)
def test_optimized_route_list_with_locks(self): def test_optimized_route_list_with_locks(self):
self.delivery_trip.delivery_stops[0].lock = 1 self.delivery_trip.delivery_stops[0].locked = 1
self.delivery_trip.save() self.delivery_trip.save()
route_list = self.delivery_trip.form_route_list(optimize=True) route_list = self.delivery_trip.form_route_list(optimize=True)

View File

@@ -255,7 +255,7 @@ class Item(Document):
# add item taxes from template # add item taxes from template
for d in template.get("taxes"): for d in template.get("taxes"):
self.append("taxes", {"item_tax_template": d.item_tax_template}) self.append("taxes", d)
# copy re-order table if empty # copy re-order table if empty
if not self.get("reorder_levels"): if not self.get("reorder_levels"):

View File

@@ -6,7 +6,6 @@ frappe.ui.form.on("Item Price", {
frm.set_query("item_code", function() { frm.set_query("item_code", function() {
return { return {
filters: { filters: {
"disabled": 0,
"has_variants": 0 "has_variants": 0
} }
}; };

View File

@@ -401,6 +401,7 @@ def make_purchase_order(source_name, target_doc=None, args=None):
["uom", "uom"], ["uom", "uom"],
["sales_order", "sales_order"], ["sales_order", "sales_order"],
["sales_order_item", "sales_order_item"], ["sales_order_item", "sales_order_item"],
["wip_composite_asset", "wip_composite_asset"],
], ],
"postprocess": update_item, "postprocess": update_item,
"condition": select_item, "condition": select_item,

View File

@@ -37,6 +37,10 @@
"rate", "rate",
"col_break3", "col_break3",
"amount", "amount",
"accounting_details_section",
"expense_account",
"column_break_glru",
"wip_composite_asset",
"manufacture_details", "manufacture_details",
"manufacturer", "manufacturer",
"manufacturer_part_no", "manufacturer_part_no",
@@ -50,11 +54,10 @@
"lead_time_date", "lead_time_date",
"sales_order", "sales_order",
"sales_order_item", "sales_order_item",
"col_break4",
"production_plan", "production_plan",
"material_request_plan_item", "material_request_plan_item",
"job_card_item", "job_card_item",
"col_break4",
"expense_account",
"section_break_46", "section_break_46",
"page_break" "page_break"
], ],
@@ -454,13 +457,28 @@
"label": "Job Card Item", "label": "Job Card Item",
"no_copy": 1, "no_copy": 1,
"print_hide": 1 "print_hide": 1
},
{
"fieldname": "accounting_details_section",
"fieldtype": "Section Break",
"label": "Accounting Details"
},
{
"fieldname": "column_break_glru",
"fieldtype": "Column Break"
},
{
"fieldname": "wip_composite_asset",
"fieldtype": "Link",
"label": "WIP Composite Asset",
"options": "Asset"
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-05-07 20:23:31.250252", "modified": "2023-10-27 15:53:41.444236",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Material Request Item", "name": "Material Request Item",
@@ -471,4 +489,4 @@
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -265,7 +265,8 @@ frappe.ui.form.on('Pick List', {
from_date: moment(frm.doc.creation).format('YYYY-MM-DD'), from_date: moment(frm.doc.creation).format('YYYY-MM-DD'),
to_date: to_date, to_date: to_date,
voucher_type: "Sales Order", voucher_type: "Sales Order",
against_pick_list: frm.doc.name, from_voucher_type: "Pick List",
from_voucher_no: frm.doc.name,
} }
frappe.set_route("query-report", "Reserved Stock"); frappe.set_route("query-report", "Reserved Stock");
} }

View File

@@ -229,20 +229,27 @@ class PickList(Document):
def create_stock_reservation_entries(self, notify=True) -> None: def create_stock_reservation_entries(self, notify=True) -> None:
"""Creates Stock Reservation Entries for Sales Order Items against Pick List.""" """Creates Stock Reservation Entries for Sales Order Items against Pick List."""
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( so_items_details_map = {}
create_stock_reservation_entries_for_so_items,
)
so_details = {}
for location in self.locations: for location in self.locations:
if location.warehouse and location.sales_order and location.sales_order_item: if location.warehouse and location.sales_order and location.sales_order_item:
so_details.setdefault(location.sales_order, []).append(location) item_details = {
"name": location.sales_order_item,
"item_code": location.item_code,
"warehouse": location.warehouse,
"qty_to_reserve": (flt(location.picked_qty) - flt(location.stock_reserved_qty)),
"from_voucher_no": location.parent,
"from_voucher_detail_no": location.name,
"serial_and_batch_bundle": location.serial_and_batch_bundle,
}
so_items_details_map.setdefault(location.sales_order, []).append(item_details)
if so_details: if so_items_details_map:
for so, locations in so_details.items(): for so, items_details in so_items_details_map.items():
so_doc = frappe.get_doc("Sales Order", so) so_doc = frappe.get_doc("Sales Order", so)
create_stock_reservation_entries_for_so_items( so_doc.create_stock_reservation_entries(
so=so_doc, items_details=locations, against_pick_list=True, notify=notify items_details=items_details,
from_voucher_type="Pick List",
notify=notify,
) )
@frappe.whitelist() @frappe.whitelist()
@@ -253,7 +260,9 @@ class PickList(Document):
cancel_stock_reservation_entries, cancel_stock_reservation_entries,
) )
cancel_stock_reservation_entries(against_pick_list=self.name, notify=notify) cancel_stock_reservation_entries(
from_voucher_type="Pick List", from_voucher_no=self.name, notify=notify
)
def validate_picked_qty(self, data): def validate_picked_qty(self, data):
over_delivery_receipt_allowance = 100 + flt( over_delivery_receipt_allowance = 100 + flt(

View File

@@ -2,7 +2,7 @@ def get_data():
return { return {
"fieldname": "pick_list", "fieldname": "pick_list",
"non_standard_fieldnames": { "non_standard_fieldnames": {
"Stock Reservation Entry": "against_pick_list", "Stock Reservation Entry": "from_voucher_no",
}, },
"internal_links": { "internal_links": {
"Sales Order": ["locations", "sales_order"], "Sales Order": ["locations", "sales_order"],

View File

@@ -264,6 +264,7 @@ class PurchaseReceipt(BuyingController):
self.make_gl_entries() self.make_gl_entries()
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
self.set_consumed_qty_in_subcontract_order() self.set_consumed_qty_in_subcontract_order()
self.reserve_stock_for_sales_order()
def check_next_docstatus(self): def check_next_docstatus(self):
submit_rv = frappe.db.sql( submit_rv = frappe.db.sql(
@@ -828,6 +829,37 @@ class PurchaseReceipt(BuyingController):
pr_doc = self if (pr == self.name) else frappe.get_doc("Purchase Receipt", pr) pr_doc = self if (pr == self.name) else frappe.get_doc("Purchase Receipt", pr)
update_billing_percentage(pr_doc, update_modified=update_modified) update_billing_percentage(pr_doc, update_modified=update_modified)
def reserve_stock_for_sales_order(self):
if self.is_return or not cint(
frappe.db.get_single_value("Stock Settings", "auto_reserve_stock_for_sales_order_on_purchase")
):
return
self.reload() # reload to get the Serial and Batch Bundle Details
so_items_details_map = {}
for item in self.items:
if item.sales_order and item.sales_order_item:
item_details = {
"name": item.sales_order_item,
"item_code": item.item_code,
"warehouse": item.warehouse,
"qty_to_reserve": item.stock_qty,
"from_voucher_no": item.parent,
"from_voucher_detail_no": item.name,
"serial_and_batch_bundle": item.serial_and_batch_bundle,
}
so_items_details_map.setdefault(item.sales_order, []).append(item_details)
if so_items_details_map:
for so, items_details in so_items_details_map.items():
so_doc = frappe.get_doc("Sales Order", so)
so_doc.create_stock_reservation_entries(
items_details=items_details,
from_voucher_type="Purchase Receipt",
notify=True,
)
def update_billed_amount_based_on_po(po_details, update_modified=True, pr_doc=None): def update_billed_amount_based_on_po(po_details, update_modified=True, pr_doc=None):
po_billed_amt_details = get_billed_amount_against_po(po_details) po_billed_amt_details = get_billed_amount_against_po(po_details)
@@ -1092,6 +1124,7 @@ def make_purchase_invoice(source_name, target_doc=None):
"is_fixed_asset": "is_fixed_asset", "is_fixed_asset": "is_fixed_asset",
"asset_location": "asset_location", "asset_location": "asset_location",
"asset_category": "asset_category", "asset_category": "asset_category",
"wip_composite_asset": "wip_composite_asset",
}, },
"postprocess": update_item, "postprocess": update_item,
"filter": lambda d: get_pending_qty(d)[0] <= 0 "filter": lambda d: get_pending_qty(d)[0] <= 0

View File

@@ -10,6 +10,7 @@ def get_data():
"Landed Cost Voucher": "receipt_document", "Landed Cost Voucher": "receipt_document",
"Auto Repeat": "reference_document", "Auto Repeat": "reference_document",
"Purchase Receipt": "return_against", "Purchase Receipt": "return_against",
"Stock Reservation Entry": "from_voucher_no",
}, },
"internal_links": { "internal_links": {
"Material Request": ["items", "material_request"], "Material Request": ["items", "material_request"],
@@ -18,7 +19,10 @@ def get_data():
"Quality Inspection": ["items", "quality_inspection"], "Quality Inspection": ["items", "quality_inspection"],
}, },
"transactions": [ "transactions": [
{"label": _("Related"), "items": ["Purchase Invoice", "Landed Cost Voucher", "Asset"]}, {
"label": _("Related"),
"items": ["Purchase Invoice", "Landed Cost Voucher", "Asset", "Stock Reservation Entry"],
},
{ {
"label": _("Reference"), "label": _("Reference"),
"items": ["Material Request", "Purchase Order", "Quality Inspection", "Project"], "items": ["Material Request", "Purchase Order", "Quality Inspection", "Project"],

View File

@@ -958,17 +958,33 @@ class TestPurchaseReceipt(FrappeTestCase):
pr1.cancel() pr1.cancel()
def test_stock_transfer_from_purchase_receipt(self): def test_stock_transfer_from_purchase_receipt(self):
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
prepare_data_for_internal_transfer()
customer = "_Test Internal Customer 2"
company = "_Test Company with perpetual inventory"
pr1 = make_purchase_receipt( pr1 = make_purchase_receipt(
warehouse="Work In Progress - TCP1", company="_Test Company with perpetual inventory" warehouse="Stores - TCP1", company="_Test Company with perpetual inventory"
) )
pr = make_purchase_receipt( dn1 = create_delivery_note(
company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", do_not_save=1 item_code=pr1.items[0].item_code,
company=company,
customer=customer,
cost_center="Main - TCP1",
expense_account="Cost of Goods Sold - TCP1",
qty=5,
rate=500,
warehouse="Stores - TCP1",
target_warehouse="Work In Progress - TCP1",
) )
pr.supplier_warehouse = "" pr = make_inter_company_purchase_receipt(dn1.name)
pr.items[0].from_warehouse = "Work In Progress - TCP1" pr.items[0].from_warehouse = "Work In Progress - TCP1"
pr.items[0].warehouse = "Stores - TCP1"
pr.submit() pr.submit()
gl_entries = get_gl_entries("Purchase Receipt", pr.name) gl_entries = get_gl_entries("Purchase Receipt", pr.name)
@@ -982,9 +998,13 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty) self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty)
pr.cancel() pr.cancel()
pr1.cancel()
def test_stock_transfer_from_purchase_receipt_with_valuation(self): def test_stock_transfer_from_purchase_receipt_with_valuation(self):
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
prepare_data_for_internal_transfer()
create_warehouse( create_warehouse(
"_Test Warehouse for Valuation", "_Test Warehouse for Valuation",
company="_Test Company with perpetual inventory", company="_Test Company with perpetual inventory",
@@ -992,16 +1012,28 @@ class TestPurchaseReceipt(FrappeTestCase):
) )
pr1 = make_purchase_receipt( pr1 = make_purchase_receipt(
warehouse="_Test Warehouse for Valuation - TCP1", warehouse="Stores - TCP1",
company="_Test Company with perpetual inventory", company="_Test Company with perpetual inventory",
) )
pr = make_purchase_receipt( customer = "_Test Internal Customer 2"
company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", do_not_save=1 company = "_Test Company with perpetual inventory"
dn1 = create_delivery_note(
item_code=pr1.items[0].item_code,
company=company,
customer=customer,
cost_center="Main - TCP1",
expense_account="Cost of Goods Sold - TCP1",
qty=5,
rate=50,
warehouse="Stores - TCP1",
target_warehouse="_Test Warehouse for Valuation - TCP1",
) )
pr = make_inter_company_purchase_receipt(dn1.name)
pr.items[0].from_warehouse = "_Test Warehouse for Valuation - TCP1" pr.items[0].from_warehouse = "_Test Warehouse for Valuation - TCP1"
pr.supplier_warehouse = "" pr.items[0].warehouse = "Stores - TCP1"
pr.append( pr.append(
"taxes", "taxes",
@@ -1037,7 +1069,6 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertEqual(gle.credit, expected_gle[i][2]) self.assertEqual(gle.credit, expected_gle[i][2])
pr.cancel() pr.cancel()
pr1.cancel()
def test_po_to_pi_and_po_to_pr_worflow_full(self): def test_po_to_pi_and_po_to_pr_worflow_full(self):
"""Test following behaviour: """Test following behaviour:
@@ -2142,6 +2173,21 @@ class TestPurchaseReceipt(FrappeTestCase):
for entry in gl_entries: for entry in gl_entries:
self.assertEqual(abs(entry.debit + entry.credit), abs(sl_entries[0].stock_value_difference)) self.assertEqual(abs(entry.debit + entry.credit), abs(sl_entries[0].stock_value_difference))
def non_internal_transfer_purchase_receipt(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
pr_doc = make_purchase_receipt(do_not_submit=True)
warehouse = create_warehouse("Internal Transfer Warehouse", pr_doc.company)
pr_doc.items[0].db_set("target_warehouse", "warehouse")
pr_doc.reload()
self.assertEqual(pr_doc.items[0].from_warehouse, warehouse.name)
pr_doc.save()
pr_doc.reload()
self.assertFalse(pr_doc.items[0].from_warehouse)
def prepare_data_for_internal_transfer(): def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -125,7 +125,9 @@
"dimension_col_break", "dimension_col_break",
"cost_center", "cost_center",
"section_break_80", "section_break_80",
"page_break" "page_break",
"sales_order",
"sales_order_item"
], ],
"fields": [ "fields": [
{ {
@@ -898,7 +900,8 @@
"label": "Delivery Note Item", "label": "Delivery Note Item",
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1,
"search_index": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
@@ -1062,12 +1065,32 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "WIP Composite Asset", "label": "WIP Composite Asset",
"options": "Asset" "options": "Asset"
},
{
"fieldname": "sales_order",
"fieldtype": "Link",
"label": "Sales Order",
"no_copy": 1,
"options": "Sales Order",
"print_hide": 1,
"read_only": 1,
"search_index": 1
},
{
"fieldname": "sales_order_item",
"fieldtype": "Data",
"hidden": 1,
"label": "Sales Order Item",
"no_copy": 1,
"print_hide": 1,
"read_only": 1,
"search_index": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-10-03 21:11:50.547261", "modified": "2023-10-30 17:32:24.560337",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Purchase Receipt Item", "name": "Purchase Receipt Item",
@@ -1078,4 +1101,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [] "states": []
} }

View File

@@ -658,7 +658,7 @@ class SerialandBatchBundle(Document):
if not available_batches: if not available_batches:
return return
available_batches = get_availabel_batches_qty(available_batches) available_batches = get_available_batches_qty(available_batches)
for batch_no in batches: for batch_no in batches:
if batch_no not in available_batches or available_batches[batch_no] < 0: if batch_no not in available_batches or available_batches[batch_no] < 0:
self.throw_error_message( self.throw_error_message(
@@ -1074,7 +1074,7 @@ def get_auto_data(**kwargs):
return get_auto_batch_nos(kwargs) return get_auto_batch_nos(kwargs)
def get_availabel_batches_qty(available_batches): def get_available_batches_qty(available_batches):
available_batches_qty = defaultdict(float) available_batches_qty = defaultdict(float)
for batch in available_batches: for batch in available_batches:
available_batches_qty[batch.batch_no] += batch.qty available_batches_qty[batch.batch_no] += batch.qty
@@ -1301,6 +1301,7 @@ def get_reserved_batches_for_pos(kwargs) -> dict:
"POS Invoice", "POS Invoice",
fields=[ fields=[
"`tabPOS Invoice Item`.batch_no", "`tabPOS Invoice Item`.batch_no",
"`tabPOS Invoice Item`.qty",
"`tabPOS Invoice`.is_return", "`tabPOS Invoice`.is_return",
"`tabPOS Invoice Item`.warehouse", "`tabPOS Invoice Item`.warehouse",
"`tabPOS Invoice Item`.name as child_docname", "`tabPOS Invoice Item`.name as child_docname",
@@ -1321,9 +1322,6 @@ def get_reserved_batches_for_pos(kwargs) -> dict:
if pos_invoice.serial_and_batch_bundle if pos_invoice.serial_and_batch_bundle
] ]
if not ids:
return {}
if ids: if ids:
for d in get_serial_batch_ledgers(kwargs.item_code, docstatus=1, name=ids): for d in get_serial_batch_ledgers(kwargs.item_code, docstatus=1, name=ids):
key = (d.batch_no, d.warehouse) key = (d.batch_no, d.warehouse)
@@ -1337,6 +1335,7 @@ def get_reserved_batches_for_pos(kwargs) -> dict:
else: else:
pos_batches[key].qty += d.qty pos_batches[key].qty += d.qty
# POS invoices having batch without bundle (to handle old POS invoices)
for row in pos_invoices: for row in pos_invoices:
if not row.batch_no: if not row.batch_no:
continue continue
@@ -1346,11 +1345,11 @@ def get_reserved_batches_for_pos(kwargs) -> dict:
key = (row.batch_no, row.warehouse) key = (row.batch_no, row.warehouse)
if key in pos_batches: if key in pos_batches:
pos_batches[key] -= row.qty * -1 if row.is_return else row.qty pos_batches[key]["qty"] -= row.qty * -1 if row.is_return else row.qty
else: else:
pos_batches[key] = frappe._dict( pos_batches[key] = frappe._dict(
{ {
"qty": (row.qty * -1 if row.is_return else row.qty), "qty": (row.qty * -1 if not row.is_return else row.qty),
"warehouse": row.warehouse, "warehouse": row.warehouse,
} }
) )

View File

@@ -1014,14 +1014,34 @@ class StockEntry(StockController):
& (se.docstatus == 1) & (se.docstatus == 1)
& (se_detail.item_code == se_item.item_code) & (se_detail.item_code == se_item.item_code)
& ( & (
(se.purchase_order == self.purchase_order) ((se.purchase_order == self.purchase_order) & (se_detail.po_detail == se_item.po_detail))
if self.subcontract_data.order_doctype == "Purchase Order" if self.subcontract_data.order_doctype == "Purchase Order"
else (se.subcontracting_order == self.subcontracting_order) else (
(se.subcontracting_order == self.subcontracting_order)
& (se_detail.sco_rm_detail == se_item.sco_rm_detail)
)
) )
) )
).run()[0][0] ).run()[0][0] or 0
if flt(total_supplied, precision) > flt(total_allowed, precision): total_returned = 0
if self.subcontract_data.order_doctype == "Subcontracting Order":
total_returned = (
frappe.qb.from_(se)
.inner_join(se_detail)
.on(se.name == se_detail.parent)
.select(Sum(se_detail.transfer_qty))
.where(
(se.purpose == "Material Transfer")
& (se.docstatus == 1)
& (se.is_return == 1)
& (se_detail.item_code == se_item.item_code)
& (se_detail.sco_rm_detail == se_item.sco_rm_detail)
& (se.subcontracting_order == self.subcontracting_order)
)
).run()[0][0] or 0
if flt(total_supplied - total_returned, precision) > flt(total_allowed, precision):
frappe.throw( frappe.throw(
_("Row {0}# Item {1} cannot be transferred more than {2} against {3} {4}").format( _("Row {0}# Item {1} cannot be transferred more than {2} against {3} {4}").format(
se_item.idx, se_item.idx,

View File

@@ -123,13 +123,6 @@ frappe.ui.form.on("Stock Reconciliation", {
fieldname: "item_code", fieldname: "item_code",
fieldtype: "Link", fieldtype: "Link",
options: "Item", options: "Item",
"get_query": function() {
return {
"filters": {
"disabled": 0,
}
};
}
}, },
{ {
label: __("Ignore Empty Stock"), label: __("Ignore Empty Stock"),

View File

@@ -92,7 +92,7 @@ frappe.ui.form.on('Stock Reservation Entry', {
'qty', 'read_only', frm.doc.has_serial_no 'qty', 'read_only', frm.doc.has_serial_no
); );
frm.set_df_property('sb_entries', 'allow_on_submit', frm.doc.against_pick_list ? 0 : 1); frm.set_df_property('sb_entries', 'allow_on_submit', frm.doc.from_voucher_type == "Pick List" ? 0 : 1);
}, },
hide_rate_related_fields(frm) { hide_rate_related_fields(frm) {

View File

@@ -17,8 +17,9 @@
"voucher_no", "voucher_no",
"voucher_detail_no", "voucher_detail_no",
"column_break_7dxj", "column_break_7dxj",
"against_pick_list", "from_voucher_type",
"against_pick_list_item", "from_voucher_no",
"from_voucher_detail_no",
"section_break_xt4m", "section_break_xt4m",
"stock_uom", "stock_uom",
"column_break_grdt", "column_break_grdt",
@@ -158,7 +159,7 @@
"oldfieldname": "actual_qty", "oldfieldname": "actual_qty",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"print_width": "150px", "print_width": "150px",
"read_only_depends_on": "eval: ((doc.reservation_based_on == \"Serial and Batch\") || (doc.against_pick_list) || (doc.delivered_qty > 0))", "read_only_depends_on": "eval: ((doc.reservation_based_on == \"Serial and Batch\") || (doc.from_voucher_type == \"Pick List\") || (doc.delivered_qty > 0))",
"width": "150px" "width": "150px"
}, },
{ {
@@ -268,27 +269,7 @@
"label": "Reservation Based On", "label": "Reservation Based On",
"no_copy": 1, "no_copy": 1,
"options": "Qty\nSerial and Batch", "options": "Qty\nSerial and Batch",
"read_only_depends_on": "eval: (doc.delivered_qty > 0 || doc.against_pick_list)" "read_only_depends_on": "eval: (doc.delivered_qty > 0 || doc.from_voucher_type == \"Pick List\")"
},
{
"fieldname": "against_pick_list",
"fieldtype": "Link",
"label": "Against Pick List",
"no_copy": 1,
"options": "Pick List",
"print_hide": 1,
"read_only": 1,
"report_hide": 1,
"search_index": 1
},
{
"fieldname": "against_pick_list_item",
"fieldtype": "Data",
"label": "Against Pick List Item",
"no_copy": 1,
"print_hide": 1,
"read_only": 1,
"report_hide": 1
}, },
{ {
"fieldname": "column_break_7dxj", "fieldname": "column_break_7dxj",
@@ -297,6 +278,36 @@
{ {
"fieldname": "column_break_grdt", "fieldname": "column_break_grdt",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "from_voucher_type",
"fieldtype": "Select",
"label": "From Voucher Type",
"no_copy": 1,
"options": "\nPick List\nPurchase Receipt",
"print_hide": 1,
"read_only": 1,
"report_hide": 1
},
{
"fieldname": "from_voucher_detail_no",
"fieldtype": "Data",
"label": "From Voucher Detail No",
"no_copy": 1,
"print_hide": 1,
"read_only": 1,
"report_hide": 1
},
{
"fieldname": "from_voucher_no",
"fieldtype": "Dynamic Link",
"label": "From Voucher No",
"no_copy": 1,
"options": "from_voucher_type",
"print_hide": 1,
"read_only": 1,
"report_hide": 1,
"search_index": 1
} }
], ],
"hide_toolbar": 1, "hide_toolbar": 1,
@@ -304,7 +315,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-08-08 17:15:13.317706", "modified": "2023-10-19 16:41:16.545416",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Reservation Entry", "name": "Stock Reservation Entry",

View File

@@ -1,6 +1,8 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
from typing import Literal
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
@@ -113,7 +115,7 @@ class StockReservationEntry(Document):
"""Auto pick Serial and Batch Nos to reserve when `Reservation Based On` is `Serial and Batch`.""" """Auto pick Serial and Batch Nos to reserve when `Reservation Based On` is `Serial and Batch`."""
if ( if (
not self.against_pick_list not self.from_voucher_type
and (self.get("_action") == "submit") and (self.get("_action") == "submit")
and (self.has_serial_no or self.has_batch_no) and (self.has_serial_no or self.has_batch_no)
and cint(frappe.db.get_single_value("Stock Settings", "auto_reserve_serial_and_batch")) and cint(frappe.db.get_single_value("Stock Settings", "auto_reserve_serial_and_batch"))
@@ -239,7 +241,7 @@ class StockReservationEntry(Document):
if available_qty_to_reserve <= 0: if available_qty_to_reserve <= 0:
msg = _( msg = _(
"Row #{0}: Stock not availabe to reserve for Item {1} against Batch {2} in Warehouse {3}." "Row #{0}: Stock not available to reserve for Item {1} against Batch {2} in Warehouse {3}."
).format( ).format(
entry.idx, entry.idx,
frappe.bold(self.item_code), frappe.bold(self.item_code),
@@ -316,21 +318,24 @@ class StockReservationEntry(Document):
) -> None: ) -> None:
"""Updates total reserved qty in the Pick List.""" """Updates total reserved qty in the Pick List."""
if self.against_pick_list and self.against_pick_list_item: if (
self.from_voucher_type == "Pick List" and self.from_voucher_no and self.from_voucher_detail_no
):
sre = frappe.qb.DocType("Stock Reservation Entry") sre = frappe.qb.DocType("Stock Reservation Entry")
reserved_qty = ( reserved_qty = (
frappe.qb.from_(sre) frappe.qb.from_(sre)
.select(Sum(sre.reserved_qty)) .select(Sum(sre.reserved_qty))
.where( .where(
(sre.docstatus == 1) (sre.docstatus == 1)
& (sre.against_pick_list == self.against_pick_list) & (sre.from_voucher_type == "Pick List")
& (sre.against_pick_list_item == self.against_pick_list_item) & (sre.from_voucher_no == self.from_voucher_no)
& (sre.from_voucher_detail_no == self.from_voucher_detail_no)
) )
).run(as_list=True)[0][0] or 0 ).run(as_list=True)[0][0] or 0
frappe.db.set_value( frappe.db.set_value(
"Pick List Item", "Pick List Item",
self.against_pick_list_item, self.from_voucher_detail_no,
reserved_qty_field, reserved_qty_field,
reserved_qty, reserved_qty,
update_modified=update_modified, update_modified=update_modified,
@@ -365,7 +370,7 @@ class StockReservationEntry(Document):
).format(self.status, self.doctype) ).format(self.status, self.doctype)
frappe.throw(msg) frappe.throw(msg)
if self.against_pick_list: if self.from_voucher_type == "Pick List":
msg = _( msg = _(
"Stock Reservation Entry created against a Pick List cannot be updated. If you need to make changes, we recommend canceling the existing entry and creating a new one." "Stock Reservation Entry created against a Pick List cannot be updated. If you need to make changes, we recommend canceling the existing entry and creating a new one."
) )
@@ -761,25 +766,27 @@ def has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: st
def create_stock_reservation_entries_for_so_items( def create_stock_reservation_entries_for_so_items(
so: object, sales_order: object,
items_details: list[dict] = None, items_details: list[dict] = None,
against_pick_list: bool = False, from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None,
notify=True, notify=True,
) -> None: ) -> None:
"""Creates Stock Reservation Entries for Sales Order Items.""" """Creates Stock Reservation Entries for Sales Order Items."""
from erpnext.selling.doctype.sales_order.sales_order import get_unreserved_qty from erpnext.selling.doctype.sales_order.sales_order import get_unreserved_qty
if not against_pick_list and ( if not from_voucher_type and (
so.get("_action") == "submit" sales_order.get("_action") == "submit"
and so.set_warehouse and sales_order.set_warehouse
and cint(frappe.get_cached_value("Warehouse", so.set_warehouse, "is_group")) and cint(frappe.get_cached_value("Warehouse", sales_order.set_warehouse, "is_group"))
): ):
return frappe.msgprint( return frappe.msgprint(
_("Stock cannot be reserved in the group warehouse {0}.").format(frappe.bold(so.set_warehouse)) _("Stock cannot be reserved in the group warehouse {0}.").format(
frappe.bold(sales_order.set_warehouse)
)
) )
validate_stock_reservation_settings(so) validate_stock_reservation_settings(sales_order)
allow_partial_reservation = frappe.db.get_single_value( allow_partial_reservation = frappe.db.get_single_value(
"Stock Settings", "allow_partial_reservation" "Stock Settings", "allow_partial_reservation"
@@ -788,38 +795,36 @@ def create_stock_reservation_entries_for_so_items(
items = [] items = []
if items_details: if items_details:
for item in items_details: for item in items_details:
so_item = frappe.get_doc( so_item = frappe.get_doc("Sales Order Item", item.get("name"))
"Sales Order Item", item.get("sales_order_item") if against_pick_list else item.get("name")
)
so_item.reserve_stock = 1
so_item.warehouse = item.get("warehouse") so_item.warehouse = item.get("warehouse")
so_item.qty_to_reserve = ( so_item.qty_to_reserve = (
item.get("picked_qty") - item.get("stock_reserved_qty", 0) flt(item.get("qty_to_reserve"))
if against_pick_list if from_voucher_type in ["Pick List", "Purchase Receipt"]
else (flt(item.get("qty_to_reserve")) * flt(so_item.conversion_factor, 1)) else (
flt(item.get("qty_to_reserve"))
* (flt(item.get("conversion_factor")) or flt(so_item.conversion_factor) or 1)
)
) )
so_item.from_voucher_no = item.get("from_voucher_no")
if against_pick_list: so_item.from_voucher_detail_no = item.get("from_voucher_detail_no")
so_item.pick_list = item.get("parent") so_item.serial_and_batch_bundle = item.get("serial_and_batch_bundle")
so_item.pick_list_item = item.get("name")
so_item.pick_list_sbb = item.get("serial_and_batch_bundle")
items.append(so_item) items.append(so_item)
sre_count = 0 sre_count = 0
reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", so.name) reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", sales_order.name)
for item in items if items_details else so.get("items"): for item in items if items_details else sales_order.get("items"):
# Skip if `Reserved Stock` is not checked for the item. # Skip if `Reserved Stock` is not checked for the item.
if not item.get("reserve_stock"): if not item.get("reserve_stock"):
continue continue
# Stock should be reserved from the Pick List if has Picked Qty. # Stock should be reserved from the Pick List if has Picked Qty.
if not against_pick_list and flt(item.picked_qty) > 0: if not from_voucher_type == "Pick List" and flt(item.picked_qty) > 0:
frappe.throw( frappe.throw(
_( _("Row #{0}: Item {1} has been picked, please reserve stock from the Pick List.").format(
"Row #{0}: Item {1} has been picked, please create a Stock Reservation from the Pick List." item.idx, frappe.bold(item.item_code)
).format(item.idx, frappe.bold(item.item_code)) )
) )
is_stock_item, has_serial_no, has_batch_no = frappe.get_cached_value( is_stock_item, has_serial_no, has_batch_no = frappe.get_cached_value(
@@ -828,13 +833,15 @@ def create_stock_reservation_entries_for_so_items(
# Skip if Non-Stock Item. # Skip if Non-Stock Item.
if not is_stock_item: if not is_stock_item:
frappe.msgprint( if not from_voucher_type:
_("Row #{0}: Stock cannot be reserved for a non-stock Item {1}").format( frappe.msgprint(
item.idx, frappe.bold(item.item_code) _("Row #{0}: Stock cannot be reserved for a non-stock Item {1}").format(
), item.idx, frappe.bold(item.item_code)
title=_("Stock Reservation"), ),
indicator="yellow", title=_("Stock Reservation"),
) indicator="yellow",
)
item.db_set("reserve_stock", 0) item.db_set("reserve_stock", 0)
continue continue
@@ -853,13 +860,15 @@ def create_stock_reservation_entries_for_so_items(
# Stock is already reserved for the item, notify the user and skip the item. # Stock is already reserved for the item, notify the user and skip the item.
if unreserved_qty <= 0: if unreserved_qty <= 0:
frappe.msgprint( if not from_voucher_type:
_("Row #{0}: Stock is already reserved for the Item {1}.").format( frappe.msgprint(
item.idx, frappe.bold(item.item_code) _("Row #{0}: Stock is already reserved for the Item {1}.").format(
), item.idx, frappe.bold(item.item_code)
title=_("Stock Reservation"), ),
indicator="yellow", title=_("Stock Reservation"),
) indicator="yellow",
)
continue continue
available_qty_to_reserve = get_available_qty_to_reserve(item.item_code, item.warehouse) available_qty_to_reserve = get_available_qty_to_reserve(item.item_code, item.warehouse)
@@ -867,7 +876,7 @@ def create_stock_reservation_entries_for_so_items(
# No stock available to reserve, notify the user and skip the item. # No stock available to reserve, notify the user and skip the item.
if available_qty_to_reserve <= 0: if available_qty_to_reserve <= 0:
frappe.msgprint( frappe.msgprint(
_("Row #{0}: No available stock to reserve for the Item {1} in Warehouse {2}.").format( _("Row #{0}: Stock not available to reserve for the Item {1} in Warehouse {2}.").format(
item.idx, frappe.bold(item.item_code), frappe.bold(item.warehouse) item.idx, frappe.bold(item.item_code), frappe.bold(item.warehouse)
), ),
title=_("Stock Reservation"), title=_("Stock Reservation"),
@@ -893,7 +902,9 @@ def create_stock_reservation_entries_for_so_items(
# Partial Reservation # Partial Reservation
if qty_to_be_reserved < unreserved_qty: if qty_to_be_reserved < unreserved_qty:
if not item.get("qty_to_reserve") or qty_to_be_reserved < flt(item.get("qty_to_reserve")): if not from_voucher_type and (
not item.get("qty_to_reserve") or qty_to_be_reserved < flt(item.get("qty_to_reserve"))
):
msg = _("Row #{0}: Only {1} available to reserve for the Item {2}").format( msg = _("Row #{0}: Only {1} available to reserve for the Item {2}").format(
item.idx, item.idx,
frappe.bold(str(qty_to_be_reserved / item.conversion_factor) + " " + item.uom), frappe.bold(str(qty_to_be_reserved / item.conversion_factor) + " " + item.uom),
@@ -915,33 +926,42 @@ def create_stock_reservation_entries_for_so_items(
sre.warehouse = item.warehouse sre.warehouse = item.warehouse
sre.has_serial_no = has_serial_no sre.has_serial_no = has_serial_no
sre.has_batch_no = has_batch_no sre.has_batch_no = has_batch_no
sre.voucher_type = so.doctype sre.voucher_type = sales_order.doctype
sre.voucher_no = so.name sre.voucher_no = sales_order.name
sre.voucher_detail_no = item.name sre.voucher_detail_no = item.name
sre.available_qty = available_qty_to_reserve sre.available_qty = available_qty_to_reserve
sre.voucher_qty = item.stock_qty sre.voucher_qty = item.stock_qty
sre.reserved_qty = qty_to_be_reserved sre.reserved_qty = qty_to_be_reserved
sre.company = so.company sre.company = sales_order.company
sre.stock_uom = item.stock_uom sre.stock_uom = item.stock_uom
sre.project = so.project sre.project = sales_order.project
if against_pick_list: if from_voucher_type:
sre.against_pick_list = item.pick_list sre.from_voucher_type = from_voucher_type
sre.against_pick_list_item = item.pick_list_item sre.from_voucher_no = item.from_voucher_no
sre.from_voucher_detail_no = item.from_voucher_detail_no
if item.pick_list_sbb: if item.get("serial_and_batch_bundle"):
sbb = frappe.get_doc("Serial and Batch Bundle", item.pick_list_sbb) sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle)
sre.reservation_based_on = "Serial and Batch" sre.reservation_based_on = "Serial and Batch"
for entry in sbb.entries:
sre.append( index, picked_qty = 0, 0
"sb_entries", while index < len(sbb.entries) and picked_qty < qty_to_be_reserved:
{ entry = sbb.entries[index]
"serial_no": entry.serial_no, qty = 1 if has_serial_no else min(abs(entry.qty), qty_to_be_reserved - picked_qty)
"batch_no": entry.batch_no,
"qty": 1 if has_serial_no else abs(entry.qty), sre.append(
"warehouse": entry.warehouse, "sb_entries",
}, {
) "serial_no": entry.serial_no,
"batch_no": entry.batch_no,
"qty": qty,
"warehouse": entry.warehouse,
},
)
index += 1
picked_qty += qty
sre.save() sre.save()
sre.submit() sre.submit()
@@ -956,29 +976,37 @@ def cancel_stock_reservation_entries(
voucher_type: str = None, voucher_type: str = None,
voucher_no: str = None, voucher_no: str = None,
voucher_detail_no: str = None, voucher_detail_no: str = None,
against_pick_list: str = None, from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None,
from_voucher_no: str = None,
from_voucher_detail_no: str = None,
sre_list: list[dict] = None, sre_list: list[dict] = None,
notify: bool = True, notify: bool = True,
) -> None: ) -> None:
"""Cancel Stock Reservation Entries.""" """Cancel Stock Reservation Entries."""
if not sre_list and against_pick_list: if not sre_list:
sre = frappe.qb.DocType("Stock Reservation Entry") if voucher_type and voucher_no:
sre_list = ( sre_list = get_stock_reservation_entries_for_voucher(
frappe.qb.from_(sre) voucher_type, voucher_no, voucher_detail_no, fields=["name"]
.select(sre.name) )
.where( elif from_voucher_type and from_voucher_no:
(sre.docstatus == 1) sre = frappe.qb.DocType("Stock Reservation Entry")
& (sre.against_pick_list == against_pick_list) query = (
& (sre.status.notin(["Delivered", "Cancelled"])) frappe.qb.from_(sre)
.select(sre.name)
.where(
(sre.docstatus == 1)
& (sre.from_voucher_type == from_voucher_type)
& (sre.from_voucher_no == from_voucher_no)
& (sre.status.notin(["Delivered", "Cancelled"]))
)
.orderby(sre.creation)
) )
.orderby(sre.creation)
).run(as_dict=True)
elif not sre_list and (voucher_type and voucher_no): if from_voucher_detail_no:
sre_list = get_stock_reservation_entries_for_voucher( query = query.where(sre.from_voucher_detail_no == from_voucher_detail_no)
voucher_type, voucher_no, voucher_detail_no, fields=["name"]
) sre_list = query.run(as_dict=True)
if sre_list: if sre_list:
for sre in sre_list: for sre in sre_list:

View File

@@ -5,6 +5,7 @@ from random import randint
import frappe import frappe
from frappe.tests.utils import FrappeTestCase, change_settings from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import today
from erpnext.selling.doctype.sales_order.sales_order import create_pick_list, make_delivery_note from erpnext.selling.doctype.sales_order.sales_order import create_pick_list, make_delivery_note
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
@@ -28,10 +29,6 @@ class TestStockReservationEntry(FrappeTestCase):
items={self.sr_item.name: self.sr_item}, warehouse=self.warehouse, qty=100 items={self.sr_item.name: self.sr_item}, warehouse=self.warehouse, qty=100
) )
def tearDown(self) -> None:
cancel_all_stock_reservation_entries()
return super().tearDown()
@change_settings("Stock Settings", {"allow_negative_stock": 0}) @change_settings("Stock Settings", {"allow_negative_stock": 0})
def test_validate_stock_reservation_settings(self) -> None: def test_validate_stock_reservation_settings(self) -> None:
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
@@ -555,8 +552,9 @@ class TestStockReservationEntry(FrappeTestCase):
(sre.voucher_type == "Sales Order") (sre.voucher_type == "Sales Order")
& (sre.voucher_no == location.sales_order) & (sre.voucher_no == location.sales_order)
& (sre.voucher_detail_no == location.sales_order_item) & (sre.voucher_detail_no == location.sales_order_item)
& (sre.against_pick_list == pl.name) & (sre.from_voucher_type == "Pick List")
& (sre.against_pick_list_item == location.name) & (sre.from_voucher_no == pl.name)
& (sre.from_voucher_detail_no == location.name)
) )
).run(as_dict=True) ).run(as_dict=True)
reserved_sb_details: set[tuple] = { reserved_sb_details: set[tuple] = {
@@ -567,6 +565,90 @@ class TestStockReservationEntry(FrappeTestCase):
# Test - 3: Reserved Serial/Batch Nos should be equal to Picked Serial/Batch Nos. # Test - 3: Reserved Serial/Batch Nos should be equal to Picked Serial/Batch Nos.
self.assertSetEqual(picked_sb_details, reserved_sb_details) self.assertSetEqual(picked_sb_details, reserved_sb_details)
@change_settings(
"Stock Settings",
{
"allow_negative_stock": 0,
"enable_stock_reservation": 1,
"auto_reserve_serial_and_batch": 1,
"pick_serial_and_batch_based_on": "FIFO",
"auto_reserve_stock_for_sales_order_on_purchase": 1,
},
)
def test_stock_reservation_from_purchase_receipt(self):
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
from erpnext.selling.doctype.sales_order.sales_order import make_material_request
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
items_details = create_items()
create_material_receipt(items_details, self.warehouse, qty=10)
item_list = []
for item_code, properties in items_details.items():
item_list.append(
{
"item_code": item_code,
"warehouse": self.warehouse,
"qty": randint(11, 100),
"uom": properties.stock_uom,
"rate": randint(10, 400),
}
)
so = make_sales_order(
item_list=item_list,
warehouse=self.warehouse,
)
mr = make_material_request(so.name)
mr.schedule_date = today()
mr.save().submit()
po = make_purchase_order(mr.name)
po.supplier = "_Test Supplier"
po.save().submit()
pr = make_purchase_receipt(po.name)
pr.save().submit()
for item in pr.items:
sre, status, reserved_qty = frappe.db.get_value(
"Stock Reservation Entry",
{
"from_voucher_type": "Purchase Receipt",
"from_voucher_no": pr.name,
"from_voucher_detail_no": item.name,
},
["name", "status", "reserved_qty"],
)
# Test - 1: SRE status should be `Reserved`.
self.assertEqual(status, "Reserved")
# Test - 2: SRE Reserved Qty should be equal to PR Item Qty.
self.assertEqual(reserved_qty, item.qty)
if item.serial_and_batch_bundle:
sb_details = frappe.db.get_all(
"Serial and Batch Entry",
filters={"parent": item.serial_and_batch_bundle},
fields=["serial_no", "batch_no", "qty"],
as_list=True,
)
reserved_sb_details = frappe.db.get_all(
"Serial and Batch Entry",
filters={"parent": sre},
fields=["serial_no", "batch_no", "qty"],
as_list=True,
)
# Test - 3: Reserved Serial/Batch Nos should be equal to PR Item Serial/Batch Nos.
self.assertEqual(set(sb_details), set(reserved_sb_details))
def tearDown(self) -> None:
cancel_all_stock_reservation_entries()
return super().tearDown()
def create_items() -> dict: def create_items() -> dict:
items_properties = [ items_properties = [

View File

@@ -38,8 +38,8 @@
"stock_reservation_tab", "stock_reservation_tab",
"enable_stock_reservation", "enable_stock_reservation",
"column_break_rx3e", "column_break_rx3e",
"auto_reserve_stock_for_sales_order",
"allow_partial_reservation", "allow_partial_reservation",
"auto_reserve_stock_for_sales_order_on_purchase",
"serial_and_batch_reservation_section", "serial_and_batch_reservation_section",
"auto_reserve_serial_and_batch", "auto_reserve_serial_and_batch",
"serial_and_batch_item_settings_tab", "serial_and_batch_item_settings_tab",
@@ -65,8 +65,7 @@
"stock_frozen_upto_days", "stock_frozen_upto_days",
"column_break_26", "column_break_26",
"role_allowed_to_create_edit_back_dated_transactions", "role_allowed_to_create_edit_back_dated_transactions",
"stock_auth_role", "stock_auth_role"
"section_break_plhx"
], ],
"fields": [ "fields": [
{ {
@@ -356,7 +355,7 @@
{ {
"default": "1", "default": "1",
"depends_on": "eval: doc.enable_stock_reservation", "depends_on": "eval: doc.enable_stock_reservation",
"description": "If enabled, <b>Partial Stock Reservation Entries</b> can be created. For example, If you have a Sales Order of 100 units and the Available Stock is 90 units then a Stock Reservation Entry will be created for 90 units. ", "description": "Partial stock can be reserved. For example, If you have a Sales Order of 100 units and the Available Stock is 90 units then a Stock Reservation Entry will be created for 90 units. ",
"fieldname": "allow_partial_reservation", "fieldname": "allow_partial_reservation",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow Partial Reservation" "label": "Allow Partial Reservation"
@@ -383,7 +382,7 @@
{ {
"default": "1", "default": "1",
"depends_on": "eval: doc.enable_stock_reservation", "depends_on": "eval: doc.enable_stock_reservation",
"description": "If enabled, Serial and Batch Nos will be auto-reserved based on <b>Pick Serial / Batch Based On</b>", "description": "Serial and Batch Nos will be auto-reserved based on <b>Pick Serial / Batch Based On</b>",
"fieldname": "auto_reserve_serial_and_batch", "fieldname": "auto_reserve_serial_and_batch",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Auto Reserve Serial and Batch Nos" "label": "Auto Reserve Serial and Batch Nos"
@@ -393,14 +392,6 @@
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Serial and Batch Reservation" "label": "Serial and Batch Reservation"
}, },
{
"default": "0",
"depends_on": "eval: doc.enable_stock_reservation",
"description": "If enabled, <b>Stock Reservation Entries</b> will be created on submission of <b>Sales Order</b>",
"fieldname": "auto_reserve_stock_for_sales_order",
"fieldtype": "Check",
"label": "Auto Reserve Stock for Sales Order"
},
{ {
"fieldname": "conversion_factor_section", "fieldname": "conversion_factor_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
@@ -421,6 +412,14 @@
"fieldname": "allow_to_edit_stock_uom_qty_for_purchase", "fieldname": "allow_to_edit_stock_uom_qty_for_purchase",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow to Edit Stock UOM Qty for Purchase Documents" "label": "Allow to Edit Stock UOM Qty for Purchase Documents"
},
{
"default": "0",
"depends_on": "eval: doc.enable_stock_reservation",
"description": "Stock will be reserved on submission of <b>Purchase Receipt</b> created against Material Receipt for Sales Order.",
"fieldname": "auto_reserve_stock_for_sales_order_on_purchase",
"fieldtype": "Check",
"label": "Auto Reserve Stock for Sales Order on Purchase"
} }
], ],
"icon": "icon-cog", "icon": "icon-cog",
@@ -428,7 +427,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-10-01 14:22:36.136111", "modified": "2023-10-18 12:35:30.068799",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Settings", "name": "Stock Settings",
@@ -453,4 +452,4 @@
"sort_order": "ASC", "sort_order": "ASC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -268,7 +268,7 @@ def get_basic_details(args, item, overwrite_warehouse=True):
if not item: if not item:
item = frappe.get_doc("Item", args.get("item_code")) item = frappe.get_doc("Item", args.get("item_code"))
if item.variant_of: if item.variant_of and not item.taxes:
item.update_template_tables() item.update_template_tables()
item_defaults = get_item_defaults(item.name, args.company) item_defaults = get_item_defaults(item.name, args.company)
@@ -330,8 +330,12 @@ def get_basic_details(args, item, overwrite_warehouse=True):
), ),
"expense_account": expense_account "expense_account": expense_account
or get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults), or get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults),
"discount_account": get_default_discount_account(args, item_defaults), "discount_account": get_default_discount_account(
"provisional_expense_account": get_provisional_account(args, item_defaults), args, item_defaults, item_group_defaults, brand_defaults
),
"provisional_expense_account": get_provisional_account(
args, item_defaults, item_group_defaults, brand_defaults
),
"cost_center": get_default_cost_center( "cost_center": get_default_cost_center(
args, item_defaults, item_group_defaults, brand_defaults args, item_defaults, item_group_defaults, brand_defaults
), ),
@@ -685,12 +689,22 @@ def get_default_expense_account(args, item, item_group, brand):
) )
def get_provisional_account(args, item): def get_provisional_account(args, item, item_group, brand):
return item.get("default_provisional_account") or args.default_provisional_account return (
item.get("default_provisional_account")
or item_group.get("default_provisional_account")
or brand.get("default_provisional_account")
or args.default_provisional_account
)
def get_default_discount_account(args, item): def get_default_discount_account(args, item, item_group, brand):
return item.get("default_discount_account") or args.discount_account return (
item.get("default_discount_account")
or item_group.get("default_discount_account")
or brand.get("default_discount_account")
or args.discount_account
)
def get_default_deferred_account(args, item, fieldname=None): def get_default_deferred_account(args, item, fieldname=None):

View File

@@ -91,16 +91,30 @@ frappe.query_reports["Reserved Stock"] = {
}, },
}, },
{ {
fieldname: "against_pick_list", fieldname: "from_voucher_type",
label: __("Against Pick List"), label: __("From Voucher Type"),
fieldtype: "Link", fieldtype: "Link",
options: "Pick List", options: "DocType",
get_query: () => ({
filters: {
name: ["in", ["Pick List", "Purchase Receipt"]],
}
}),
},
{
fieldname: "from_voucher_no",
label: __("From Voucher No"),
fieldtype: "Dynamic Link",
options: "from_voucher_type",
get_query: () => ({ get_query: () => ({
filters: { filters: {
docstatus: 1, docstatus: 1,
company: frappe.query_report.get_filter_value("company"), company: frappe.query_report.get_filter_value("company"),
}, },
}), }),
get_options: function () {
return frappe.query_report.get_filter_value("from_voucher_type");
},
}, },
{ {
fieldname: "reservation_based_on", fieldname: "reservation_based_on",

View File

@@ -44,7 +44,8 @@ def get_data(filters):
(sre.available_qty - sre.reserved_qty).as_("available_qty"), (sre.available_qty - sre.reserved_qty).as_("available_qty"),
sre.voucher_type, sre.voucher_type,
sre.voucher_no, sre.voucher_no,
sre.against_pick_list, sre.from_voucher_type,
sre.from_voucher_no,
sre.name.as_("stock_reservation_entry"), sre.name.as_("stock_reservation_entry"),
sre.status, sre.status,
sre.project, sre.project,
@@ -65,7 +66,8 @@ def get_data(filters):
"warehouse", "warehouse",
"voucher_type", "voucher_type",
"voucher_no", "voucher_no",
"against_pick_list", "from_voucher_type",
"from_voucher_no",
"reservation_based_on", "reservation_based_on",
"status", "status",
"project", "project",
@@ -142,7 +144,6 @@ def get_columns():
"fieldname": "voucher_type", "fieldname": "voucher_type",
"label": _("Voucher Type"), "label": _("Voucher Type"),
"fieldtype": "Data", "fieldtype": "Data",
"options": "Warehouse",
"width": 110, "width": 110,
}, },
{ {
@@ -153,11 +154,17 @@ def get_columns():
"width": 120, "width": 120,
}, },
{ {
"fieldname": "against_pick_list", "fieldname": "from_voucher_type",
"label": _("Against Pick List"), "label": _("From Voucher Type"),
"fieldtype": "Link", "fieldtype": "Data",
"options": "Pick List", "width": 110,
"width": 130, },
{
"fieldname": "from_voucher_no",
"label": _("From Voucher No"),
"fieldtype": "Dynamic Link",
"options": "from_voucher_type",
"width": 120,
}, },
{ {
"fieldname": "stock_reservation_entry", "fieldname": "stock_reservation_entry",

View File

@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import copy import copy
import gzip
import json import json
from typing import Optional, Set, Tuple from typing import Optional, Set, Tuple
@@ -10,17 +11,7 @@ from frappe import _, scrub
from frappe.model.meta import get_field_precision from frappe.model.meta import get_field_precision
from frappe.query_builder import Case from frappe.query_builder import Case
from frappe.query_builder.functions import CombineDatetime, Sum from frappe.query_builder.functions import CombineDatetime, Sum
from frappe.utils import ( from frappe.utils import cint, flt, get_link_to_form, getdate, now, nowdate, parse_json
cint,
flt,
get_link_to_form,
getdate,
gzip_compress,
gzip_decompress,
now,
nowdate,
parse_json,
)
import erpnext import erpnext
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
@@ -295,7 +286,7 @@ def get_reposting_data(file_path) -> dict:
attached_file = frappe.get_doc("File", file_name) attached_file = frappe.get_doc("File", file_name)
data = gzip_decompress(attached_file.get_content()) data = gzip.decompress(attached_file.get_content())
if data := json.loads(data.decode("utf-8")): if data := json.loads(data.decode("utf-8")):
data = data data = data
@@ -378,7 +369,7 @@ def get_reposting_file_name(dt, dn):
def create_json_gz_file(data, doc, file_name=None) -> str: def create_json_gz_file(data, doc, file_name=None) -> str:
encoded_content = frappe.safe_encode(frappe.as_json(data)) encoded_content = frappe.safe_encode(frappe.as_json(data))
compressed_content = gzip_compress(encoded_content) compressed_content = gzip.compress(encoded_content)
if not file_name: if not file_name:
json_filename = f"{scrub(doc.doctype)}-{scrub(doc.name)}.json.gz" json_filename = f"{scrub(doc.doctype)}-{scrub(doc.name)}.json.gz"

View File

@@ -107,7 +107,7 @@ frappe.ui.form.on('Subcontracting Order', {
get_materials_from_supplier: function (frm) { get_materials_from_supplier: function (frm) {
let sco_rm_details = []; let sco_rm_details = [];
if (frm.doc.status != "Closed" && frm.doc.supplied_items && frm.doc.per_received > 0) { if (frm.doc.status != "Closed" && frm.doc.supplied_items) {
frm.doc.supplied_items.forEach(d => { frm.doc.supplied_items.forEach(d => {
if (d.total_supplied_qty > 0 && d.total_supplied_qty != d.consumed_qty) { if (d.total_supplied_qty > 0 && d.total_supplied_qty != d.consumed_qty) {
sco_rm_details.push(d.name); sco_rm_details.push(d.name);
@@ -193,7 +193,7 @@ erpnext.buying.SubcontractingOrderController = class SubcontractingOrderControll
} }
has_unsupplied_items() { has_unsupplied_items() {
return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty); return this.frm.doc['supplied_items'].some(item => item.required_qty > (item.supplied_qty - item.returned_qty));
} }
make_subcontracting_receipt() { make_subcontracting_receipt() {

View File

@@ -1,13 +1,6 @@
frappe.ui.form.on("Issue", { frappe.ui.form.on("Issue", {
onload: function(frm) { onload: function(frm) {
frm.email_field = "raised_by"; frm.email_field = "raised_by";
frm.set_query("customer", function () {
return {
filters: {
"disabled": 0
}
};
});
frappe.db.get_value("Support Settings", {name: "Support Settings"}, frappe.db.get_value("Support Settings", {name: "Support Settings"},
["allow_resetting_service_level_agreement", "track_service_level_agreement"], (r) => { ["allow_resetting_service_level_agreement", "track_service_level_agreement"], (r) => {

View File

@@ -14,18 +14,16 @@
{% block style %} {% block style %}
<style> <style>
{ {% include "templates/includes/projects.css" %}
% include "templates/includes/projects.css"%
}
</style> </style>
{% endblock %} {% endblock %}
{% block page_content %} {% block page_content %}
<div class="web-list-item transaction-list-item"> <div class="web-list-item transaction-list-item">
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-sm-4 "><b>Status: {{ doc.status }}</b></div> <div class="col-sm-4 "><b>{{ _("Status") }}: {{ _(doc.status) }}</b></div>
<div class="col-sm-4 "><b>Progress: {{ doc.percent_complete }}%</b></div> <div class="col-sm-4 "><b>{{ _("Progress") }}: {{ doc.get_formatted("percent_complete") }}</b></div>
<div class="col-sm-4 "><b>Hours Spent: {{ doc.actual_time | round }}</b></div> <div class="col-sm-4 "><b>{{ _("Hours Spent") }}: {{ doc.get_formatted("actual_time") }}</b></div>
</div> </div>
</div> </div>
@@ -34,7 +32,7 @@
<hr> <hr>
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-sm-6 my-account-header"> <h4>Tasks</h4></div> <div class="col-sm-6 my-account-header"> <h4>{{ _("Tasks") }}</h4></div>
<div class="col-sm-6 text-right"> <div class="col-sm-6 text-right">
<a class="btn btn-secondary btn-light btn-sm" href='/tasks/new?project={{ doc.project_name }}'>{{ _("New task") }}</a> <a class="btn btn-secondary btn-light btn-sm" href='/tasks/new?project={{ doc.project_name }}'>{{ _("New task") }}</a>
</div> </div>
@@ -44,39 +42,39 @@
<div class="result"> <div class="result">
<div class="web-list-item transaction-list-item"> <div class="web-list-item transaction-list-item">
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-sm-4"><b>Tasks</b></div> <div class="col-sm-4"><b>{{ _("Tasks") }}</b></div>
<div class="col-sm-2"><b>Status</b></div> <div class="col-sm-2"><b>{{ _("Status") }}</b></div>
<div class="col-sm-2"><b>End Date</b></div> <div class="col-sm-2"><b>{{ _("End Date") }}</b></div>
<div class="col-sm-2"><b>Assignment</b></div> <div class="col-sm-2"><b>{{ _("Assignment") }}</b></div>
<div class="col-sm-2"><b>Modified On</b></div> <div class="col-sm-2"><b>{{ _("Modified On") }}</b></div>
</div> </div>
</div> </div>
{% include "erpnext/templates/includes/projects/project_tasks.html" %} {% include "erpnext/templates/includes/projects/project_tasks.html" %}
</div> </div>
</div> </div>
{% else %} {% else %}
{{ empty_state('Task')}} {{ empty_state(_("Task")) }}
{% endif %} {% endif %}
<h4 class="my-account-header">Timesheets</h4> <h4 class="my-account-header">{{ _("Timesheets") }}</h4>
{% if doc.timesheets %} {% if doc.timesheets %}
<div class="website-list"> <div class="website-list">
<div class="result"> <div class="result">
<div class="web-list-item transaction-list-item"> <div class="web-list-item transaction-list-item">
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-xs-2"><b>Timesheet</b></div> <div class="col-xs-2"><b>{{ _("Timesheet") }}</b></div>
<div class="col-xs-2"><b>Status</b></div> <div class="col-xs-2"><b>{{ _("Status") }}</b></div>
<div class="col-xs-2"><b>From</b></div> <div class="col-xs-2"><b>{{ _("From") }}</b></div>
<div class="col-xs-2"><b>To</b></div> <div class="col-xs-2"><b>{{ _("To") }}</b></div>
<div class="col-xs-2"><b>Modified By</b></div> <div class="col-xs-2"><b>{{ _("Modified By") }}</b></div>
<div class="col-xs-2"><b>Modified On</b></div> <div class="col-xs-2"><b>{{ _("Modified On") }}</b></div>
</div> </div>
</div> </div>
{% include "erpnext/templates/includes/projects/project_timesheets.html" %} {% include "erpnext/templates/includes/projects/project_timesheets.html" %}
</div> </div>
</div> </div>
{% else %} {% else %}
{{ empty_state('Timesheet')}} {{ empty_state(_("Timesheet")) }}
{% endif %} {% endif %}
{% if doc.attachments %} {% if doc.attachments %}
@@ -113,7 +111,7 @@
{% macro progress_bar(percent_complete) %} {% macro progress_bar(percent_complete) %}
{% if percent_complete %} {% if percent_complete %}
<span class="small py-2">Project Progress:</span> <span class="small py-2">{{ _("Project Progress:") }}</span>
<div class="progress progress-hg" style="height: 15px;"> <div class="progress progress-hg" style="height: 15px;">
<div <div
class="progress-bar progress-bar-{{ 'warning' if percent_complete|round < 100 else 'success' }} active"\ class="progress-bar progress-bar-{{ 'warning' if percent_complete|round < 100 else 'success' }} active"\
@@ -133,7 +131,7 @@
<div> <div>
<img src="/assets/frappe/images/ui-states/list-empty-state.svg" alt="Generic Empty State" class="null-state"> <img src="/assets/frappe/images/ui-states/list-empty-state.svg" alt="Generic Empty State" class="null-state">
</div> </div>
<p>You haven't created a {{ section_name }} yet</p> <p>{{ _("You haven't created a {0} yet").format(section_name) }}</p>
</div> </div>
</div> </div>
</div> </div>