fix: patch conflict

This commit is contained in:
Khushi Rawat
2025-05-13 14:34:26 +05:30
58 changed files with 1907 additions and 774 deletions

View File

@@ -58,6 +58,10 @@ class TestAccountingDimension(IntegrationTestCase):
self.assertEqual(gle1.get("department"), "_Test Department - _TC")
def test_mandatory(self):
location = frappe.get_doc("Accounting Dimension", "Location")
location.dimension_defaults[0].mandatory_for_bs = True
location.save()
si = create_sales_invoice(do_not_save=1)
si.append(
"items",
@@ -121,7 +125,6 @@ def create_dimension():
"company": "_Test Company",
"reference_document": "Location",
"default_dimension": "Block 1",
"mandatory_for_bs": 1,
},
)

View File

@@ -21,7 +21,6 @@
"party_name",
"book_advance_payments_in_separate_party_account",
"reconcile_on_advance_payment_date",
"advance_reconciliation_takes_effect_on",
"column_break_11",
"bank_account",
"party_bank_account",
@@ -754,18 +753,9 @@
"options": "No\nYes",
"print_hide": 1,
"search_index": 1
},
{
"default": "Oldest Of Invoice Or Advance",
"fetch_from": "company.reconciliation_takes_effect_on",
"fieldname": "advance_reconciliation_takes_effect_on",
"fieldtype": "Select",
"hidden": 1,
"label": "Advance Reconciliation Takes Effect On",
"no_copy": 1,
"options": "Advance Payment Date\nOldest Of Invoice Or Advance\nReconciliation Date"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [
@@ -777,7 +767,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2025-03-24 16:18:19.920701",
"modified": "2025-05-08 11:18:10.238085",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",
@@ -817,6 +807,7 @@
"write": 1
}
],
"row_format": "Dynamic",
"show_name_in_global_search": 1,
"sort_field": "creation",
"sort_order": "DESC",

View File

@@ -80,9 +80,6 @@ class PaymentEntry(AccountsController):
PaymentEntryReference,
)
advance_reconciliation_takes_effect_on: DF.Literal[
"Advance Payment Date", "Oldest Of Invoice Or Advance", "Reconciliation Date"
]
amended_from: DF.Link | None
apply_tax_withholding_amount: DF.Check
auto_repeat: DF.Link | None
@@ -1574,9 +1571,12 @@ class PaymentEntry(AccountsController):
else:
# For backwards compatibility
# Supporting reposting on payment entries reconciled before select field introduction
if self.advance_reconciliation_takes_effect_on == "Advance Payment Date":
reconciliation_takes_effect_on = frappe.get_cached_value(
"Company", self.company, "reconciliation_takes_effect_on"
)
if reconciliation_takes_effect_on == "Advance Payment Date":
posting_date = self.posting_date
elif self.advance_reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
date_field = "posting_date"
if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
date_field = "transaction_date"
@@ -1586,7 +1586,7 @@ class PaymentEntry(AccountsController):
if getdate(posting_date) < getdate(self.posting_date):
posting_date = self.posting_date
elif self.advance_reconciliation_takes_effect_on == "Reconciliation Date":
elif reconciliation_takes_effect_on == "Reconciliation Date":
posting_date = nowdate()
frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date)
@@ -2448,7 +2448,7 @@ def get_outstanding_reference_documents(args, validate=False):
accounts = get_party_account(
args.get("party_type"), args.get("party"), args.get("company"), include_advance=True
)
advance_account = accounts[1] if len(accounts) >= 1 else None
advance_account = accounts[1] if len(accounts) > 1 else None
if party_account == advance_account:
party_account = accounts[0]

View File

@@ -159,6 +159,10 @@ class TestPOSClosingEntry(IntegrationTestCase):
"""
create_dimension()
location = frappe.get_doc("Accounting Dimension", "Location")
location.dimension_defaults[0].mandatory_for_bs = True
location.save()
pos_profile = make_pos_profile(do_not_insert=1, do_not_set_accounting_dimension=1)
self.assertRaises(frappe.ValidationError, pos_profile.insert)

View File

@@ -31,7 +31,7 @@
"ignore_pricing_rule",
"allow_rate_change",
"allow_discount_change",
"disable_grand_total_to_default_mop",
"set_grand_total_to_default_mop",
"section_break_23",
"item_groups",
"column_break_25",
@@ -402,12 +402,6 @@
"fieldtype": "Check",
"label": "Print Receipt on Order Complete"
},
{
"default": "0",
"fieldname": "disable_grand_total_to_default_mop",
"fieldtype": "Check",
"label": "Disable auto setting Grand Total to default Payment Mode"
},
{
"fieldname": "project",
"fieldtype": "Link",
@@ -415,6 +409,12 @@
"oldfieldname": "cost_center",
"oldfieldtype": "Link",
"options": "Project"
},
{
"default": "1",
"fieldname": "set_grand_total_to_default_mop",
"fieldtype": "Check",
"label": "Set Grand Total to Default Payment Method"
}
],
"grid_page_length": 50,
@@ -443,7 +443,7 @@
"link_fieldname": "pos_profile"
}
],
"modified": "2025-04-09 11:35:13.779613",
"modified": "2025-05-09 11:23:28.632136",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",

View File

@@ -40,7 +40,6 @@ class POSProfile(Document):
currency: DF.Link
customer: DF.Link | None
customer_groups: DF.Table[POSCustomerGroup]
disable_grand_total_to_default_mop: DF.Check
disable_rounded_total: DF.Check
disabled: DF.Check
expense_account: DF.Link | None
@@ -56,6 +55,7 @@ class POSProfile(Document):
project: DF.Link | None
select_print_heading: DF.Link | None
selling_price_list: DF.Link | None
set_grand_total_to_default_mop: DF.Check
tax_category: DF.Link | None
taxes_and_charges: DF.Link | None
tc_name: DF.Link | None

View File

@@ -1702,6 +1702,9 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
# Configure Buying Settings to allow rate change
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
# Configure Accounts Settings to allow 300% over billing
frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", 300)
# Create PR: rate = 1000, qty = 5
pr = make_purchase_receipt(
item_code="_Test Non Stock Item", rate=1000, posting_date=add_days(nowdate(), -2)
@@ -2773,6 +2776,43 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
self.assertEqual(invoice.grand_total, 300)
def test_pr_pi_over_billing(self):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as make_purchase_invoice_from_pr,
)
# Configure Buying Settings to allow rate change
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
pr = make_purchase_receipt(qty=10, rate=10)
pi = make_purchase_invoice_from_pr(pr.name)
pi.items[0].rate = 12
# Test 1 - This will fail because over billing is not allowed
self.assertRaises(frappe.ValidationError, pi.submit)
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
# Test 2 - This will now submit because over billing allowance is ignored when set_landed_cost_based_on_purchase_invoice_rate is checked
pi.submit()
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", 20)
pi.cancel()
pi = make_purchase_invoice_from_pr(pr.name)
pi.items[0].rate = 12
# Test 3 - This will now submit because over billing is allowed upto 20%
pi.submit()
pi.reload()
pi.cancel()
pi = make_purchase_invoice_from_pr(pr.name)
pi.items[0].rate = 13
# Test 4 - Since this PI is overbilled by 130% and only 120% is allowed, it will fail
self.assertRaises(frappe.ValidationError, pi.submit)
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(

View File

@@ -2230,13 +2230,13 @@ class TestSalesInvoice(ERPNextTestSuite):
self.assertEqual(expected_account_values[1], gle.credit)
def test_rounding_adjustment_3(self):
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension,
disable_dimension,
)
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import create_dimension
# Dimension creates custom field, which does an implicit DB commit as it is a DDL command
# Ensure dimension don't have any mandatory fields
create_dimension()
# rollback from tearDown() happens till here
si = create_sales_invoice(do_not_save=True)
si.items = []
for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]:
@@ -2317,8 +2317,6 @@ class TestSalesInvoice(ERPNextTestSuite):
self.assertEqual(round_off_gle.cost_center, "_Test Cost Center 2 - _TC")
self.assertEqual(round_off_gle.location, "Block 1")
disable_dimension()
def test_sales_invoice_with_shipping_rule(self):
from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule

View File

@@ -218,6 +218,8 @@ frappe.query_reports["General Ledger"] = {
fieldtype: "Check",
},
],
collapsible_filters: true,
seperate_check_filters: true,
};
erpnext.utils.add_dimensions("General Ledger", 15);

View File

@@ -2,9 +2,6 @@
# License: GNU General Public License v3. See license.txt
import copy
from collections import OrderedDict
import frappe
from frappe import _, _dict
from frappe.query_builder import Criterion
@@ -19,6 +16,15 @@ from erpnext.accounts.report.financial_statements import get_cost_centers_with_c
from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency
from erpnext.accounts.utils import get_account_currency
DEBIT_CREDIT_DICT = {
"debit": 0.0,
"credit": 0.0,
"debit_in_account_currency": 0.0,
"credit_in_account_currency": 0.0,
"debit_in_transaction_currency": None,
"credit_in_transaction_currency": None,
}
def execute(filters=None):
if not filters:
@@ -365,75 +371,83 @@ def set_bill_no(gl_entries):
gl["bill_no"] = inv_details.get(gl.get("against_voucher"), "")
def get_translated_labels_for_totals():
def wrap_in_quotes(label):
return f"'{label}'"
return {
"opening": wrap_in_quotes(_("Opening")),
"total": wrap_in_quotes(_("Total")),
"closing": wrap_in_quotes(_("Closing (Opening + Total)")),
}
def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries):
def add_total_to_data(totals, key):
row = totals[key]
row["account"] = labels[key]
data.append(row)
labels = get_translated_labels_for_totals()
data = []
totals_dict = get_totals_dict()
set_bill_no(gl_entries)
gle_map = initialize_gle_map(gl_entries, filters, totals_dict)
gle_map = initialize_gle_map(gl_entries, filters)
totals, entries = get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, totals_dict)
totals, entries = get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map)
# Opening for filtered account
data.append(totals.opening)
add_total_to_data(totals, "opening")
if filters.get("categorize_by") != "Categorize by Voucher (Consolidated)":
for _acc, acc_dict in gle_map.items():
# acc
if acc_dict.entries:
# opening
data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None})
if (not filters.get("categorize_by") and not filters.get("voucher_no")) or (
filters.get("categorize_by") and filters.get("categorize_by") != "Categorize by Voucher"
):
data.append(acc_dict.totals.opening)
set_opening_closing = (not filters.get("categorize_by") and not filters.get("voucher_no")) or (
filters.get("categorize_by") and filters.get("categorize_by") != "Categorize by Voucher"
)
set_total = filters.get("categorize_by") or not filters.voucher_no
data += acc_dict.entries
for acc_dict in gle_map.values():
if not acc_dict.entries:
continue
# totals
if filters.get("categorize_by") or not filters.voucher_no:
data.append(acc_dict.totals.total)
# opening
data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None})
if set_opening_closing:
add_total_to_data(acc_dict.totals, "opening")
# closing
if (not filters.get("categorize_by") and not filters.get("voucher_no")) or (
filters.get("categorize_by") and filters.get("categorize_by") != "Categorize by Voucher"
):
data.append(acc_dict.totals.closing)
data += acc_dict.entries
# totals
if set_total:
add_total_to_data(acc_dict.totals, "total")
# closing
if set_opening_closing:
add_total_to_data(acc_dict.totals, "closing")
data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None})
else:
data += entries
# totals
data.append(totals.total)
add_total_to_data(totals, "total")
# closing
data.append(totals.closing)
add_total_to_data(totals, "closing")
return data
def get_totals_dict():
def _get_debit_credit_dict(label):
return _dict(
account=f"'{label}'",
debit=0.0,
credit=0.0,
debit_in_account_currency=0.0,
credit_in_account_currency=0.0,
debit_in_transaction_currency=None,
credit_in_transaction_currency=None,
)
return _dict(
opening=_get_debit_credit_dict(_("Opening")),
total=_get_debit_credit_dict(_("Total")),
closing=_get_debit_credit_dict(_("Closing (Opening + Total)")),
opening=_dict(DEBIT_CREDIT_DICT),
total=_dict(DEBIT_CREDIT_DICT),
closing=_dict(DEBIT_CREDIT_DICT),
)
def group_by_field(group_by):
def get_group_by_field(group_by):
if group_by == "Categorize by Party":
return "party"
elif group_by in ["Categorize by Voucher (Consolidated)", "Categorize by Account"]:
@@ -442,19 +456,25 @@ def group_by_field(group_by):
return "voucher_no"
def initialize_gle_map(gl_entries, filters, totals_dict):
gle_map = OrderedDict()
group_by = group_by_field(filters.get("categorize_by"))
def initialize_gle_map(gl_entries, filters):
gle_map = {}
group_by = get_group_by_field(filters.get("categorize_by"))
for gle in gl_entries:
gle_map.setdefault(gle.get(group_by), _dict(totals=copy.deepcopy(totals_dict), entries=[]))
group_by_value = gle.get(group_by)
if group_by_value not in gle_map:
gle_map[group_by_value] = _dict(
totals=get_totals_dict(),
entries=[],
)
return gle_map
def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, totals):
def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
entries = []
consolidated_gle = OrderedDict()
group_by = group_by_field(filters.get("categorize_by"))
consolidated_gle = {}
group_by = get_group_by_field(filters.get("categorize_by"))
group_by_voucher_consolidated = filters.get("categorize_by") == "Categorize by Voucher (Consolidated)"
if filters.get("show_net_values_in_party_account"):
@@ -500,6 +520,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot
from_date, to_date = getdate(filters.from_date), getdate(filters.to_date)
show_opening_entries = filters.get("show_opening_entries")
totals = get_totals_dict()
for gle in gl_entries:
group_by_value = gle.get(group_by)
gle.voucher_subtype = _(gle.voucher_subtype)
@@ -566,17 +587,20 @@ def get_account_type_map(company):
def get_result_as_list(data, filters):
balance, _balance_in_account_currency = 0, 0
balance = 0
for d in data:
if not d.get("posting_date"):
balance, _balance_in_account_currency = 0, 0
balance = 0
balance = get_balance(d, balance, "debit", "credit")
d["balance"] = balance
d["account_currency"] = filters.account_currency
d["presentation_currency"] = filters.presentation_currency
return data
@@ -602,11 +626,8 @@ def get_columns(filters):
if filters.get("presentation_currency"):
currency = filters["presentation_currency"]
else:
if filters.get("company"):
currency = get_company_currency(filters["company"])
else:
company = get_default_company()
currency = get_company_currency(company)
company = filters.get("company") or get_default_company()
filters["presentation_currency"] = currency = get_company_currency(company)
columns = [
{
@@ -627,19 +648,22 @@ def get_columns(filters):
{
"label": _("Debit ({0})").format(currency),
"fieldname": "debit",
"fieldtype": "Float",
"fieldtype": "Currency",
"options": "presentation_currency",
"width": 130,
},
{
"label": _("Credit ({0})").format(currency),
"fieldname": "credit",
"fieldtype": "Float",
"fieldtype": "Currency",
"options": "presentation_currency",
"width": 130,
},
{
"label": _("Balance ({0})").format(currency),
"fieldname": "balance",
"fieldtype": "Float",
"fieldtype": "Currency",
"options": "presentation_currency",
"width": 130,
},
]

View File

@@ -35,7 +35,6 @@ def execute(filters=None):
filters=filters,
accumulated_values=filters.accumulated_values,
ignore_closing_entries=True,
ignore_accumulated_values_for_fy=True,
)
expense = get_data(
@@ -46,7 +45,6 @@ def execute(filters=None):
filters=filters,
accumulated_values=filters.accumulated_values,
ignore_closing_entries=True,
ignore_accumulated_values_for_fy=True,
)
net_profit_loss = get_net_profit_loss(

View File

@@ -4,7 +4,7 @@
import frappe
from frappe.desk.query_report import export_query
from frappe.tests import IntegrationTestCase
from frappe.utils import getdate, today
from frappe.utils import add_days, getdate, today
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.financial_statements import get_period_list
@@ -58,7 +58,7 @@ class TestProfitAndLossStatement(AccountsTestMixin, IntegrationTestCase):
period_end_date=fy.year_end_date,
filter_based_on="Fiscal Year",
periodicity="Monthly",
accumulated_vallues=True,
accumulated_values=False,
)
def test_profit_and_loss_output_and_summary(self):
@@ -109,3 +109,64 @@ class TestProfitAndLossStatement(AccountsTestMixin, IntegrationTestCase):
sales_account = frappe.db.get_value("Company", self.company, "default_income_account")
self.assertIn(sales_account, contents)
def test_accumulate_filter(self):
# ensure 2 fiscal years
cur_fy = self.get_fiscal_year()
find_for = add_days(cur_fy.year_start_date, -1)
_x = frappe.db.get_all(
"Fiscal Year",
filters={"disabled": 0, "year_start_date": ("<=", find_for), "year_end_date": (">=", find_for)},
)[0]
prev_fy = frappe.get_doc("Fiscal Year", _x.name)
prev_fy.append("companies", {"company": self.company})
prev_fy.save()
# make SI on both of them
prev_fy_si = self.create_sales_invoice(qty=1, rate=450, do_not_submit=True)
prev_fy_si.posting_date = add_days(prev_fy.year_end_date, -1)
prev_fy_si.save().submit()
income_acc = prev_fy_si.items[0].income_account
self.create_sales_invoice(qty=1, rate=120)
# Unaccumualted
filters = frappe._dict(
company=self.company,
from_fiscal_year=prev_fy.name,
to_fiscal_year=cur_fy.name,
period_start_date=prev_fy.year_start_date,
period_end_date=cur_fy.year_end_date,
filter_based_on="Date Range",
periodicity="Yearly",
accumulated_values=False,
)
result = execute(filters)
columns = [result[0][2], result[0][3]]
expected = {
"account": income_acc,
columns[0].get("fieldname"): 450.0,
columns[1].get("fieldname"): 120.0,
}
actual = [x for x in result[1] if x.get("account") == income_acc]
self.assertEqual(len(actual), 1)
actual = actual[0]
for key in expected.keys():
with self.subTest(key=key):
self.assertEqual(expected.get(key), actual.get(key))
# accumualted
filters.update({"accumulated_values": True})
expected = {
"account": income_acc,
columns[0].get("fieldname"): 450.0,
columns[1].get("fieldname"): 570.0,
}
result = execute(filters)
columns = [result[0][2], result[0][3]]
actual = [x for x in result[1] if x.get("account") == income_acc]
self.assertEqual(len(actual), 1)
actual = actual[0]
for key in expected.keys():
with self.subTest(key=key):
self.assertEqual(expected.get(key), actual.get(key))

View File

@@ -731,10 +731,13 @@ def update_reference_in_payment_entry(
update_advance_paid = []
# Update Reconciliation effect date in reference
reconciliation_takes_effect_on = frappe.get_cached_value(
"Company", payment_entry.company, "reconciliation_takes_effect_on"
)
if payment_entry.book_advance_payments_in_separate_party_account:
if payment_entry.advance_reconciliation_takes_effect_on == "Advance Payment Date":
if reconciliation_takes_effect_on == "Advance Payment Date":
reconcile_on = payment_entry.posting_date
elif payment_entry.advance_reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
date_field = "posting_date"
if d.against_voucher_type in ["Sales Order", "Purchase Order"]:
date_field = "transaction_date"
@@ -742,7 +745,7 @@ def update_reference_in_payment_entry(
if getdate(reconcile_on) < getdate(payment_entry.posting_date):
reconcile_on = payment_entry.posting_date
elif payment_entry.advance_reconciliation_takes_effect_on == "Reconciliation Date":
elif reconciliation_takes_effect_on == "Reconciliation Date":
reconcile_on = nowdate()
reference_details.update({"reconcile_effect_on": reconcile_on})

View File

@@ -408,29 +408,29 @@ scheduler_events = {
"0/15 * * * *": [
"erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs",
],
"0/30 * * * *": [
"erpnext.utilities.doctype.video.video.update_youtube_data",
],
"0/30 * * * *": [],
# Hourly but offset by 30 minutes
"30 * * * *": [
"erpnext.accounts.doctype.gl_entry.gl_entry.rename_gle_sle_docs",
],
# Daily but offset by 45 minutes
"45 0 * * *": [
"erpnext.stock.reorder_item.reorder_item",
],
"45 0 * * *": [],
},
"hourly": [
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
"erpnext.projects.doctype.project.project.project_status_update_reminder",
"erpnext.projects.doctype.project.project.hourly_reminder",
"erpnext.projects.doctype.project.project.collect_project_status",
],
"hourly_long": [
"hourly_long": [],
"hourly_maintenance": [
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
"erpnext.utilities.bulk_transaction.retry",
"erpnext.projects.doctype.project.project.collect_project_status",
"erpnext.projects.doctype.project.project.project_status_update_reminder",
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
"erpnext.utilities.doctype.video.video.update_youtube_data",
],
"daily": [
"daily": [],
"daily_long": [],
"daily_maintenance": [
"erpnext.support.doctype.issue.issue.auto_close_tickets",
"erpnext.crm.doctype.opportunity.opportunity.auto_close_opportunity",
"erpnext.controllers.accounts_controller.update_invoice_status",
@@ -454,17 +454,16 @@ scheduler_events = {
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_daily",
"erpnext.accounts.utils.run_ledger_health_checks",
"erpnext.assets.doctype.asset_maintenance_log.asset_maintenance_log.update_asset_maintenance_log_status",
],
"weekly": [
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
],
"daily_long": [
"erpnext.stock.reorder_item.reorder_item",
"erpnext.accounts.doctype.process_subscription.process_subscription.create_subscription_process",
"erpnext.setup.doctype.email_digest.email_digest.send",
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.auto_update_latest_price_in_all_boms",
"erpnext.crm.utils.open_leads_opportunities_based_on_todays_event",
"erpnext.assets.doctype.asset.depreciation.post_depreciation_entries",
],
"weekly": [
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
],
"monthly_long": [
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_monthly",

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: hello@frappe.io\n"
"POT-Creation-Date: 2025-05-04 09:35+0000\n"
"PO-Revision-Date: 2025-05-05 10:00\n"
"PO-Revision-Date: 2025-05-12 17:59\n"
"Last-Translator: hello@frappe.io\n"
"Language-Team: Persian\n"
"MIME-Version: 1.0\n"
@@ -89,7 +89,7 @@ msgstr "\"آیتم تامین شده توسط مشتری\" نمی‌تواند
#: erpnext/stock/doctype/item/item.py:237
msgid "\"Customer Provided Item\" cannot have Valuation Rate"
msgstr "\"آیتم تامین شده توسط مشتری\" نمی‌تواند دارای نرخ ارزیابی باشد"
msgstr "\"آیتم تامین شده توسط مشتری\" نمی‌تواند دارای نرخ ارزش‌گذاری باشد"
#: erpnext/stock/doctype/item/item.py:313
msgid "\"Is Fixed Asset\" cannot be unchecked, as Asset record exists against the item"
@@ -352,7 +352,7 @@ msgstr "(H) تغییر در ارزش موجودی (صف FIFO)"
#: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:209
msgid "(H) Valuation Rate"
msgstr "(H) نرخ ارزش گذاری"
msgstr "(H) نرخ ارزشگذاری"
#. Description of the 'Actual Operating Cost' (Currency) field in DocType 'Work
#. Order Operation'
@@ -363,12 +363,12 @@ msgstr "(نرخ ساعت / 60) * زمان عملیات واقعی"
#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:273
#: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:176
msgid "(I) Valuation Rate"
msgstr "(I) نرخ ارزش گذاری"
msgstr "(I) نرخ ارزشگذاری"
#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:278
#: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:181
msgid "(J) Valuation Rate as per FIFO"
msgstr "(J) نرخ ارزش گذاری مطابق با FIFO"
msgstr "(J) نرخ ارزشگذاری مطابق با FIFO"
#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:288
#: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:191
@@ -2221,7 +2221,7 @@ msgstr "اقدام اگر بازرسی کیفیت رد شود"
#. Settings'
#: erpnext/buying/doctype/buying_settings/buying_settings.json
msgid "Action If Same Rate is Not Maintained"
msgstr "اگر همان نرخ حفظ نشود، اقدام کنید"
msgstr "اقدام در صورت عدم حفظ نرخ یکسان"
#: erpnext/quality_management/doctype/quality_review/quality_review_list.js:7
msgid "Action Initialised"
@@ -4278,7 +4278,7 @@ msgstr "اجازه نرخ صفر"
#: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
#: erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
msgid "Allow Zero Valuation Rate"
msgstr "نرخ ارزش گذاری صفر مجاز است"
msgstr "نرخ ارزشگذاری صفر مجاز است"
#. Label of the allow_existing_serial_no (Check) field in DocType 'Stock
#. Settings'
@@ -4320,7 +4320,7 @@ msgstr ""
#. 'Manufacturing Settings'
#: erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
msgid "Allow transferring raw materials even after the Required Quantity is fulfilled"
msgstr "امکان انتقال مواد خام حتی پس از برآورده شدن مقدار مورد نیاز"
msgstr "اجازه انتقال مواد خام حتی پس از برآورده شدن مقدار مورد نیاز"
#. Label of the allowed (Check) field in DocType 'Repost Allowed Types'
#: erpnext/accounts/doctype/repost_allowed_types/repost_allowed_types.json
@@ -6061,7 +6061,7 @@ msgstr "در ردیف #{0}: مقدار انتخاب شده {1} برای آیتم
#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:84
msgid "At least one account with exchange gain or loss is required"
msgstr ""
msgstr "حداقل یک حساب با سود یا زیان تبدیل مورد نیاز است"
#: erpnext/assets/doctype/asset/asset.py:1123
msgid "At least one asset has to be selected."
@@ -11551,7 +11551,7 @@ msgstr "پیکربندی اسمبلی محصول"
#. DocType 'Buying Settings'
#: erpnext/buying/doctype/buying_settings/buying_settings.json
msgid "Configure the action to stop the transaction or just warn if the same rate is not maintained."
msgstr "کنش را طوری پیکربندی کنید که تراکنش را متوقف کند یا در صورت عدم حفظ همان نرخ فقط هشدار دهد."
msgstr "تنظیم کنید که در صورت عدم حفظ همان نرخ، تراکنش متوقف شود یا فقط هشدار داده شود."
#: erpnext/buying/doctype/buying_settings/buying_settings.js:20
msgid "Configure the default Price List when creating a new Purchase transaction. Item prices will be fetched from this Price List."
@@ -13693,21 +13693,21 @@ msgstr "تبدیل ارز"
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
#: erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
msgid "Currency Exchange Settings"
msgstr "تنظیمات تبادل ارز"
msgstr "تنظیمات تبدیل ارز"
#. Name of a DocType
#: erpnext/accounts/doctype/currency_exchange_settings_details/currency_exchange_settings_details.json
msgid "Currency Exchange Settings Details"
msgstr "جزئیات تنظیمات تبادل ارز"
msgstr "جزئیات تنظیمات تبدیل ارز"
#. Name of a DocType
#: erpnext/accounts/doctype/currency_exchange_settings_result/currency_exchange_settings_result.json
msgid "Currency Exchange Settings Result"
msgstr "نتیجه تنظیمات تبادل ارز"
msgstr "نتیجه تنظیمات تبدیل ارز"
#: erpnext/setup/doctype/currency_exchange/currency_exchange.py:55
msgid "Currency Exchange must be applicable for Buying or for Selling."
msgstr "مبادله ارز باید برای خرید یا فروش قابل اجرا باشد."
msgstr "تبدیل ارز باید برای خرید یا فروش قابل اجرا باشد."
#. Label of the currency_and_price_list (Section Break) field in DocType 'POS
#. Invoice'
@@ -13896,7 +13896,7 @@ msgstr "زمان کنونی"
#. Reconciliation Item'
#: erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
msgid "Current Valuation Rate"
msgstr "نرخ ارزش گذاری فعلی"
msgstr "نرخ ارزشگذاری فعلی"
#: erpnext/selling/report/sales_analytics/sales_analytics.js:90
msgid "Curves"
@@ -14297,7 +14297,7 @@ msgstr "گروه مشتریان"
#. Name of a DocType
#: erpnext/accounts/doctype/customer_group_item/customer_group_item.json
msgid "Customer Group Item"
msgstr "مورد گروه مشتری"
msgstr "آیتم گروه مشتری"
#. Label of the customer_group_name (Data) field in DocType 'Customer Group'
#: erpnext/setup/doctype/customer_group/customer_group.json
@@ -14316,7 +14316,7 @@ msgstr "گروه های مشتری"
#. Name of a DocType
#: erpnext/accounts/doctype/customer_item/customer_item.json
msgid "Customer Item"
msgstr "مورد مشتری"
msgstr "آیتم مشتری"
#. Label of the customer_items (Table) field in DocType 'Item'
#: erpnext/stock/doctype/item/item.json
@@ -15756,7 +15756,7 @@ msgstr "روزهای تاخیری"
#. Name of a report
#: erpnext/stock/report/delayed_item_report/delayed_item_report.json
msgid "Delayed Item Report"
msgstr "گزارش مورد تاخیر"
msgstr "گزارش آیتم با تاخیر"
#. Name of a report
#: erpnext/stock/report/delayed_order_report/delayed_order_report.json
@@ -17682,7 +17682,7 @@ msgstr ""
#. DocType 'Selling Settings'
#: erpnext/selling/doctype/selling_settings/selling_settings.json
msgid "Don't Reserve Sales Order Qty on Sales Return"
msgstr "تعداد سفارش فروش را در بازگشت فروش رزرو نکنید"
msgstr "عدم رزرو مقدار سفارش فروش در بازگشت فروش"
#. Label of the mute_emails (Check) field in DocType 'Bank Statement Import'
#: erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json
@@ -19170,7 +19170,7 @@ msgstr "لاگ خطا"
#. Label of the error_message (Text) field in DocType 'Period Closing Voucher'
#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json
msgid "Error Message"
msgstr "پیغام خطا"
msgstr "پیام خطا"
#: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:274
msgid "Error Occurred"
@@ -19305,17 +19305,17 @@ msgstr "زمان راه‌اندازی بیش از حد دستگاه"
#. 'Company'
#: erpnext/setup/doctype/company/company.json
msgid "Exchange Gain / Loss"
msgstr ""
msgstr "سود / زیان تبدیل"
#. Label of the exchange_gain_loss_account (Link) field in DocType 'Company'
#: erpnext/setup/doctype/company/company.json
msgid "Exchange Gain / Loss Account"
msgstr "حساب سود / زیان مبادله"
msgstr "حساب سود / زیان تبدیل"
#. Option for the 'Entry Type' (Select) field in DocType 'Journal Entry'
#: erpnext/accounts/doctype/journal_entry/journal_entry.json
msgid "Exchange Gain Or Loss"
msgstr "سود یا ضرر مبادله"
msgstr "سود یا ضرر تبدیل"
#. Label of the exchange_gain_loss (Currency) field in DocType 'Payment Entry
#. Reference'
@@ -19330,12 +19330,12 @@ msgstr "سود یا ضرر مبادله"
#: erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json
#: erpnext/setup/doctype/company/company.py:548
msgid "Exchange Gain/Loss"
msgstr "سود/زیان مبادله"
msgstr "سود/زیان تبدیل"
#: erpnext/controllers/accounts_controller.py:1590
#: erpnext/controllers/accounts_controller.py:1674
msgid "Exchange Gain/Loss amount has been booked through {0}"
msgstr "مبلغ سود/زیان مبادله از طریق {0} رزرو شده است"
msgstr "مبلغ سود/زیان تبدیل از طریق {0} رزرو شده است"
#. Label of the exchange_rate (Float) field in DocType 'Journal Entry Account'
#. Label of the exchange_rate (Float) field in DocType 'Payment Entry
@@ -19678,7 +19678,7 @@ msgstr "سر هزینه تغییر کرد"
#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:591
msgid "Expense account is mandatory for item {0}"
msgstr "حساب هزینه برای مورد {0} اجباری است"
msgstr "حساب هزینه برای آیتم {0} اجباری است"
#: erpnext/assets/doctype/asset_repair/asset_repair.py:98
msgid "Expense account not present in Purchase Invoice {0}"
@@ -20463,7 +20463,7 @@ msgstr "هزینه عملیاتی بر اساس کالاهای تمام شده"
#: erpnext/stock/doctype/stock_entry/stock_entry.py:1340
msgid "Finished Item {0} does not match with Work Order {1}"
msgstr "مورد تمام شده {0} با دستور کار {1} مطابقت ندارد"
msgstr "آیتم تمام شده {0} با دستور کار {1} مطابقت ندارد"
#. Label of the first_email (Time) field in DocType 'Project'
#: erpnext/projects/doctype/project/project.json
@@ -20809,11 +20809,11 @@ msgstr "برای دستور کار"
#: erpnext/controllers/status_updater.py:267
msgid "For an item {0}, quantity must be negative number"
msgstr "برای یک مورد {0}، مقدار باید عدد منفی باشد"
msgstr "برای یک آیتم {0}، مقدار باید عدد منفی باشد"
#: erpnext/controllers/status_updater.py:264
msgid "For an item {0}, quantity must be positive number"
msgstr "برای یک مورد {0}، مقدار باید عدد مثبت باشد"
msgstr "برای یک آیتم {0}، مقدار باید عدد مثبت باشد"
#. Description of the 'Income Account' (Link) field in DocType 'Dunning'
#: erpnext/accounts/doctype/dunning/dunning.json
@@ -23231,7 +23231,7 @@ msgstr ""
#. field in DocType 'Stock Settings'
#: erpnext/stock/doctype/stock_settings/stock_settings.json
msgid "If enabled, the item rate won't adjust to the valuation rate during internal transfers, but accounting will still use the valuation rate."
msgstr "اگر فعال شود، نرخ آیتم در انتقالات داخلی با نرخ ارزیابی تنظیم نخواهد شد، اما حسابداری همچنان از نرخ ارزیابی استفاده خواهد کرد."
msgstr "اگر فعال شود، نرخ آیتم در انتقالات داخلی با نرخ ارزش‌گذاری تنظیم نخواهد شد، اما حسابداری همچنان از نرخ ارزش‌گذاری استفاده خواهد کرد."
#. Description of the 'Allow UOM with Conversion Rate Defined in Item' (Check)
#. field in DocType 'Stock Settings'
@@ -23253,7 +23253,7 @@ msgstr ""
#. DocType 'Stock Settings'
#: erpnext/stock/doctype/stock_settings/stock_settings.json
msgid "If enabled, the system will use the moving average valuation method to calculate the valuation rate for the batched items and will not consider the individual batch-wise incoming rate."
msgstr "اگر فعال شود، سیستم از روش ارزیابی میانگین متحرک برای محاسبه نرخ ارزیابی آیتم‌های دسته‌ای استفاده خواهد کرد و نرخ ورودی هر دسته را به‌طور جداگانه در نظر نخواهد گرفت."
msgstr "اگر فعال شود، سیستم از روش ارزیابی میانگین متحرک برای محاسبه نرخ ارزش‌گذاری آیتم‌های دسته‌ای استفاده خواهد کرد و نرخ ورودی هر دسته را به‌طور جداگانه در نظر نخواهد گرفت."
#. Description of the 'Validate Applied Rule' (Check) field in DocType 'Pricing
#. Rule'
@@ -23310,7 +23310,7 @@ msgstr "اگر حساب مسدود شود، ورود به کاربران محد
#: erpnext/stock/stock_ledger.py:1859
msgid "If the item is transacting as a Zero Valuation Rate item in this entry, please enable 'Allow Zero Valuation Rate' in the {0} Item table."
msgstr "اگر آیتم به عنوان یک آیتم نرخ ارزش گذاری صفر در این ثبت تراکنش می‌شود، لطفاً \"نرخ ارزش گذاری صفر مجاز\" را در جدول آیتم {0} فعال کنید."
msgstr "اگر آیتم به عنوان یک آیتم نرخ ارزشگذاری صفر در این ثبت تراکنش می‌شود، لطفاً \"نرخ ارزشگذاری صفر مجاز\" را در جدول آیتم {0} فعال کنید."
#: erpnext/manufacturing/doctype/work_order/work_order.js:1088
msgid "If the selected BOM has Operations mentioned in it, the system will fetch all Operations from BOM, these values can be changed."
@@ -23373,7 +23373,7 @@ msgstr "اگر علامت زده شود، می‌توان از چندین ماد
#: erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.js:36
msgid "If ticked, the BOM cost will be automatically updated based on Valuation Rate / Price List Rate / last purchase rate of raw materials."
msgstr "در صورت علامت زدن، هزینه BOM به طور خودکار بر اساس نرخ ارزش گذاری / نرخ لیست قیمت / آخرین نرخ خرید مواد اولیه به روز می‌شود."
msgstr "در صورت علامت زدن، هزینه BOM به طور خودکار بر اساس نرخ ارزشگذاری / نرخ لیست قیمت / آخرین نرخ خرید مواد اولیه به روز می‌شود."
#: erpnext/accounts/doctype/loyalty_program/loyalty_program.js:14
msgid "If unlimited expiry for the Loyalty Points, keep the Expiry Duration empty or 0."
@@ -25436,7 +25436,7 @@ msgstr "تخفیف داده شده است"
#. Deduction'
#: erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json
msgid "Is Exchange Gain / Loss?"
msgstr ""
msgstr "سود / زیان تبدیل است؟"
#. Label of the is_existing_asset (Check) field in DocType 'Asset'
#: erpnext/assets/doctype/asset/asset.json
@@ -27037,7 +27037,7 @@ msgstr "تعداد مورد را نمی‌توان به روز کرد زیرا
#: erpnext/stock/doctype/stock_entry/stock_entry.py:865
msgid "Item rate has been updated to zero as Allow Zero Valuation Rate is checked for item {0}"
msgstr "نرخ آیتم به صفر به‌روزرسانی شده است زیرا نرخ ارزش گذاری مجاز صفر برای آیتم صفر {0} بررسی می‌شود"
msgstr "نرخ آیتم به صفر به‌روزرسانی شده است زیرا نرخ ارزشگذاری مجاز صفر برای آیتم صفر {0} بررسی می‌شود"
#. Label of the finished_good (Link) field in DocType 'Job Card'
#: erpnext/manufacturing/doctype/job_card/job_card.json
@@ -27067,7 +27067,7 @@ msgstr "آیتم {0} را نمی‌توان به عنوان یک زیر مونت
#: erpnext/manufacturing/doctype/blanket_order/blanket_order.py:197
msgid "Item {0} cannot be ordered more than {1} against Blanket Order {2}."
msgstr "مورد {0} را نمی‌توان بیش از {1} در مقابل سفارش بلانکت {2} سفارش داد."
msgstr "آیتم {0} را نمی‌توان بیش از {1} در مقابل سفارش کلی {2} سفارش داد."
#: erpnext/assets/doctype/asset/asset.py:268
#: erpnext/stock/doctype/item/item.py:630
@@ -27113,19 +27113,19 @@ msgstr "مورد {0} قبلاً در برابر سفارش فروش {1} رزرو
#: erpnext/stock/doctype/item/item.py:1146
msgid "Item {0} is cancelled"
msgstr "مورد {0} لغو شده است"
msgstr "آیتم {0} لغو شده است"
#: erpnext/stock/doctype/item/item.py:1130
msgid "Item {0} is disabled"
msgstr "مورد {0} غیرفعال است"
msgstr "آیتم {0} غیرفعال است"
#: erpnext/selling/doctype/installation_note/installation_note.py:79
msgid "Item {0} is not a serialized Item"
msgstr "مورد {0} یک مورد سریالی نیست"
msgstr "آیتم {0} یک آیتم سریالی نیست"
#: erpnext/stock/doctype/item/item.py:1138
msgid "Item {0} is not a stock Item"
msgstr "مورد {0} یک مورد موجودی نیست"
msgstr "آیتم {0} یک آیتم موجودی نیست"
#: erpnext/manufacturing/doctype/production_plan/production_plan.py:875
msgid "Item {0} is not a subcontracted item"
@@ -27133,7 +27133,7 @@ msgstr "آیتم {0} یک آیتم قرارداد فرعی شده نیست"
#: erpnext/stock/doctype/stock_entry/stock_entry.py:1701
msgid "Item {0} is not active or end of life has been reached"
msgstr "مورد {0} فعال نیست یا به پایان عمر رسیده است"
msgstr "آیتم {0} فعال نیست یا به پایان عمر رسیده است"
#: erpnext/assets/doctype/asset/asset.py:272
msgid "Item {0} must be a Fixed Asset Item"
@@ -27157,7 +27157,7 @@ msgstr "مورد {0} در جدول \"مواد خام تامین شده\" در {1
#: erpnext/stock/doctype/item_price/item_price.py:56
msgid "Item {0} not found."
msgstr "مورد {0} یافت نشد."
msgstr "آیتم {0} یافت نشد."
#: erpnext/buying/doctype/purchase_order/purchase_order.py:343
msgid "Item {0}: Ordered qty {1} cannot be less than minimum order qty {2} (defined in Item)."
@@ -27206,7 +27206,7 @@ msgstr "ثبت فروش بر حسب آیتم"
#: erpnext/manufacturing/doctype/bom/bom.py:346
msgid "Item: {0} does not exist in the system"
msgstr "مورد: {0} در سیستم وجود ندارد"
msgstr "آیتم: {0} در سیستم وجود ندارد"
#. Label of the items_section (Section Break) field in DocType 'POS Invoice'
#. Label of the items (Table) field in DocType 'POS Invoice'
@@ -27317,7 +27317,7 @@ msgstr "آیتم‌ها برای درخواست مواد خام"
#: erpnext/stock/doctype/stock_entry/stock_entry.py:861
msgid "Items rate has been updated to zero as Allow Zero Valuation Rate is checked for the following items: {0}"
msgstr "نرخ آیتم‌ها به صفر به‌روزرسانی شده است زیرا نرخ ارزش گذاری مجاز صفر برای آیتم‌های زیر بررسی می‌شود: {0}"
msgstr "نرخ آیتم‌ها به صفر به‌روزرسانی شده است زیرا نرخ ارزشگذاری مجاز صفر برای آیتم‌های زیر بررسی می‌شود: {0}"
#. Label of the items_to_be_repost (Code) field in DocType 'Repost Item
#. Valuation'
@@ -27622,7 +27622,7 @@ msgstr "ثبت دفتر روزنامه {0} دارای حساب {1} نیست یا
#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js:97
msgid "Journal entries have been created"
msgstr ""
msgstr "ثبت‌های دفتر روزنامه ایجاد شده است"
#. Label of the journals_section (Section Break) field in DocType 'Accounts
#. Settings'
@@ -27762,7 +27762,7 @@ msgstr ""
#. Name of a UOM
#: erpnext/setup/setup_wizard/data/uom_data.json
msgid "Knot"
msgstr ""
msgstr "گره دریایی"
#. Option for the 'Valuation Method' (Select) field in DocType 'Item'
#. Option for the 'Default Valuation Method' (Select) field in DocType 'Stock
@@ -28797,7 +28797,7 @@ msgstr "حفظ همان نرخ در کل چرخه فروش"
#. Label of the maintain_same_rate (Check) field in DocType 'Buying Settings'
#: erpnext/buying/doctype/buying_settings/buying_settings.json
msgid "Maintain Same Rate Throughout the Purchase Cycle"
msgstr "همان نرخ را در طول چرخه خرید حفظ کنید"
msgstr "حفظ همان نرخ در طول چرخه خرید"
#. Label of the is_stock_item (Check) field in DocType 'Item'
#: erpnext/stock/doctype/item/item.json
@@ -30017,7 +30017,7 @@ msgstr "حداکثر تخفیف برای آیتم {0} {1}% است"
#: erpnext/public/js/utils/barcode_scanner.js:99
msgid "Maximum quantity scanned for item {0}."
msgstr "حداکثر مقدار اسکن شده برای مورد {0}."
msgstr "حداکثر مقدار اسکن شده برای آیتم {0}."
#. Description of the 'Max Sample Quantity' (Int) field in DocType 'Item'
#: erpnext/stock/doctype/item/item.json
@@ -30083,7 +30083,7 @@ msgstr "مگاوات"
#: erpnext/stock/stock_ledger.py:1872
msgid "Mention Valuation Rate in the Item master."
msgstr "نرخ ارزش گذاری را در آیتم اصلی ذکر کنید."
msgstr "نرخ ارزشگذاری را در آیتم اصلی ذکر کنید."
#. Description of the 'Accounts' (Table) field in DocType 'Customer'
#: erpnext/selling/doctype/customer/customer.json
@@ -30176,7 +30176,7 @@ msgstr "نمونه های پیام"
#: erpnext/accounts/doctype/payment_request/payment_request.js:47
#: erpnext/setup/doctype/email_digest/email_digest.js:26
msgid "Message Sent"
msgstr "پیغام فرستاده شد"
msgstr "پیام فرستاده شد"
#. Label of the message_for_supplier (Text Editor) field in DocType 'Request
#. for Quotation'
@@ -31211,7 +31211,7 @@ msgstr "مقدار منفی مجاز نیست"
#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:573
msgid "Negative Valuation Rate is not allowed"
msgstr "نرخ ارزش گذاری منفی مجاز نیست"
msgstr "نرخ ارزشگذاری منفی مجاز نیست"
#: erpnext/setup/setup_wizard/data/sales_stage.txt:8
#: erpnext/setup/setup_wizard/operations/install_fixtures.py:418
@@ -32286,7 +32286,7 @@ msgstr "یادداشت"
#: erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js:21
msgid "Note: Automatic log deletion only applies to logs of type <i>Update Cost</i>"
msgstr "توجه: حذف خودکار لاگ فقط برای گزارش‌هایی از نوع <i>به‌روزرسانی هزینه</i> اعمال می‌شود"
msgstr "توجه: حذف خودکار لاگ فقط برای لاگ‌هایی از نوع <i>به‌روزرسانی هزینه</i> اعمال می‌شود"
#: erpnext/accounts/party.py:691
msgid "Note: Due Date exceeds allowed customer credit days by {0} day(s)"
@@ -34073,7 +34073,7 @@ msgstr "آیتم فاکتور POS"
#. Name of a DocType
#: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json
msgid "POS Invoice Merge Log"
msgstr "گزارش ادغام فاکتور POS"
msgstr "لاگ ادغام فاکتور POS"
#. Name of a DocType
#: erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json
@@ -35961,13 +35961,13 @@ msgstr "تخصیص درصد باید برابر با 100٪ باشد"
#. 'Buying Settings'
#: erpnext/buying/doctype/buying_settings/buying_settings.json
msgid "Percentage you are allowed to order beyond the Blanket Order quantity."
msgstr "درصدی که مجاز به سفارش آن هستید فراتر از مقدار سفارش کلی."
msgstr "درصدی که اجازه دارید بیش از مقدار سفارش کلی سفارش دهید."
#. Description of the 'Blanket Order Allowance (%)' (Float) field in DocType
#. 'Selling Settings'
#: erpnext/selling/doctype/selling_settings/selling_settings.json
msgid "Percentage you are allowed to sell beyond the Blanket Order quantity."
msgstr "درصدی که مجاز به فروش آن هستید فراتر از مقدار سفارش کلی."
msgstr "درصدی که اجازه دارید بیش از مقدار سفارش کلی بفروشید."
#. Description of the 'Over Transfer Allowance (%)' (Float) field in DocType
#. 'Buying Settings'
@@ -36396,7 +36396,7 @@ msgstr "X روز قبل عملیات را برنامه ریزی کنید"
#. Settings'
#: erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
msgid "Plan time logs outside Workstation working hours"
msgstr "گزارش های زمان را خارج از ساعات کاری ایستگاه کاری برنامه ریزی کنید"
msgstr "برنامه ریزی لاگ‌های زمان خارج از ساعات کاری ایستگاه کاری"
#. Label of the quantity (Float) field in DocType 'Material Request Plan Item'
#: erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json
@@ -37014,7 +37014,7 @@ msgstr "لطفا ابتدا شرکت را انتخاب کنید"
#: erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py:52
msgid "Please select Completion Date for Completed Asset Maintenance Log"
msgstr "لطفاً تاریخ تکمیل را برای گزارش کامل تعمیر و نگهداری دارایی انتخاب کنید"
msgstr "لطفاً تاریخ تکمیل را برای لاگ تعمیر و نگهداری دارایی کامل شده انتخاب کنید"
#: erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js:84
#: erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js:125
@@ -37337,7 +37337,7 @@ msgstr "لطفاً شناسه مالیاتی را برای مشتری \"%s\" ت
#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:338
msgid "Please set Unrealized Exchange Gain/Loss Account in Company {0}"
msgstr "لطفاً حساب سود/زیان غیرواقعی مبادله را در شرکت تنظیم کنید {0}"
msgstr "لطفاً حساب سود/زیان تبدیل تحقق نیافته را در شرکت {0} تنظیم کنید"
#: erpnext/regional/report/vat_audit_report/vat_audit_report.py:56
msgid "Please set VAT Accounts in {0}"
@@ -37410,7 +37410,7 @@ msgstr "لطفاً حساب پیش‌فرض نقدی یا بانکی را در
#: erpnext/accounts/utils.py:2218
msgid "Please set default Exchange Gain/Loss Account in Company {}"
msgstr "لطفاً حساب سود/زیان مبادله پیش‌فرض را در شرکت تنظیم کنید {}"
msgstr "لطفاً حساب سود/زیان تبدیل پیش‌فرض را در شرکت تنظیم کنید {}"
#: erpnext/assets/doctype/asset_repair/asset_repair.py:363
msgid "Please set default Expense Account in Company {0}"
@@ -37498,7 +37498,7 @@ msgstr "لطفاً {0} را در BOM Creator {1} تنظیم کنید"
#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1219
msgid "Please set {0} in Company {1} to account for Exchange Gain / Loss"
msgstr ""
msgstr "لطفاً {0} را در شرکت {1} برای محاسبه سود / زیان تبدیل تنظیم کنید"
#: erpnext/controllers/accounts_controller.py:521
msgid "Please set {0} to {1}, the same account that was used in the original invoice {2}."
@@ -37542,7 +37542,7 @@ msgstr "لطفا حداقل یک ویژگی را در جدول Attributes مشخ
#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:563
msgid "Please specify either Quantity or Valuation Rate or both"
msgstr "لطفاً مقدار یا نرخ ارزش گذاری یا هر دو را مشخص کنید"
msgstr "لطفاً مقدار یا نرخ ارزشگذاری یا هر دو را مشخص کنید"
#: erpnext/stock/doctype/item_attribute/item_attribute.py:93
msgid "Please specify from/to range"
@@ -38725,7 +38725,7 @@ msgstr "احتمال (%)"
#: erpnext/manufacturing/doctype/workstation/workstation.json
#: erpnext/quality_management/doctype/quality_action_resolution/quality_action_resolution.json
msgid "Problem"
msgstr "مسئله"
msgstr "مشکل"
#. Label of the procedure (Link) field in DocType 'Non Conformance'
#. Label of the procedure (Link) field in DocType 'Quality Action'
@@ -38813,7 +38813,7 @@ msgstr "فرآیند تطبیق پرداخت"
#. Name of a DocType
#: erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.json
msgid "Process Payment Reconciliation Log"
msgstr "گزارش تطبیق پرداخت فرآیند"
msgstr "پردازش لاگ تطبیق پرداخت"
#. Name of a DocType
#: erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.json
@@ -44933,7 +44933,7 @@ msgstr "ردیف #{0}: زمان‌بندی با ردیف {1} در تضاد اس
#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:97
msgid "Row #{0}: You cannot use the inventory dimension '{1}' in Stock Reconciliation to modify the quantity or valuation rate. Stock reconciliation with inventory dimensions is intended solely for performing opening entries."
msgstr "ردیف #{0}: نمی‌توانید از بعد موجودی «{1}» در تطبیق موجودی برای تغییر مقدار یا نرخ ارزیابی استفاده کنید. تطبیق موجودی با ابعاد موجودی صرفاً برای انجام ورودی های افتتاحیه در نظر گرفته شده است."
msgstr "ردیف #{0}: نمی‌توانید از بعد موجودی «{1}» در تطبیق موجودی برای تغییر مقدار یا نرخ ارزش‌گذاری استفاده کنید. تطبیق موجودی با ابعاد موجودی صرفاً برای انجام ورودی های افتتاحیه در نظر گرفته شده است."
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1526
msgid "Row #{0}: You must select an Asset for Item {1}."
@@ -44965,7 +44965,7 @@ msgstr "ردیف #{idx}: هنگام تامین مواد خام به پیمانک
#: erpnext/controllers/buying_controller.py:406
msgid "Row #{idx}: Item rate has been updated as per valuation rate since its an internal stock transfer."
msgstr "ردیف #{idx}: نرخ آیتم براساس نرخ ارزیابی به‌روزرسانی شده است، زیرا یک انتقال داخلی موجودی است."
msgstr "ردیف #{idx}: نرخ آیتم براساس نرخ ارزش‌گذاری به‌روزرسانی شده است، زیرا یک انتقال داخلی موجودی است."
#: erpnext/controllers/buying_controller.py:870
msgid "Row #{idx}: Please enter a location for the asset item {item_code}."
@@ -45224,7 +45224,7 @@ msgstr "ردیف {0}: الگوی مالیات آیتم بر اساس اعتبا
#: erpnext/controllers/selling_controller.py:541
msgid "Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
msgstr "ردیف {0}: نرخ اقلام براساس نرخ ارزیابی به‌روزرسانی شده است، زیرا یک انتقال داخلی موجودی است"
msgstr "ردیف {0}: نرخ اقلام براساس نرخ ارزش‌گذاری به‌روزرسانی شده است، زیرا یک انتقال داخلی موجودی است"
#: erpnext/controllers/subcontracting_controller.py:98
msgid "Row {0}: Item {1} must be a stock item."
@@ -45705,7 +45705,7 @@ msgstr "فاکتور پیش پرداخت فروش"
#: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
#: erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
msgid "Sales Invoice Item"
msgstr "مورد فاکتور فروش"
msgstr "آیتم فاکتور فروش"
#. Label of the sales_invoice_no (Link) field in DocType 'Stock Entry'
#: erpnext/stock/doctype/stock_entry/stock_entry.json
@@ -46000,7 +46000,7 @@ msgstr "روند سفارش فروش"
#: erpnext/stock/doctype/delivery_note/delivery_note.py:253
msgid "Sales Order required for Item {0}"
msgstr "سفارش فروش برای مورد {0} لازم است"
msgstr "سفارش فروش برای آیتم {0} لازم است"
#: erpnext/selling/doctype/sales_order/sales_order.py:277
msgid "Sales Order {0} already exists against Customer's Purchase Order {1}. To allow multiple Sales Orders, Enable {2} in {3}"
@@ -46443,7 +46443,7 @@ msgstr "همان کالا و ترکیب انبار قبلا وارد شده اس
#: erpnext/buying/utils.py:61
msgid "Same item cannot be entered multiple times."
msgstr "یک مورد را نمی‌توان چندین بار وارد کرد."
msgstr "یک آیتم را نمی‌توان چندین بار وارد کرد."
#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:79
msgid "Same supplier has been entered multiple times"
@@ -46578,7 +46578,7 @@ msgstr "اسکن شماره سریال"
#: erpnext/public/js/utils/barcode_scanner.js:179
msgid "Scan barcode for item {0}"
msgstr "اسکن بارکد برای مورد {0}"
msgstr "اسکن بارکد برای آیتم {0}"
#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js:106
msgid "Scan mode enabled, existing quantity will not be fetched."
@@ -48007,12 +48007,12 @@ msgstr "UOM آیتم خدمات"
#: erpnext/subcontracting/doctype/subcontracting_bom/subcontracting_bom.py:64
msgid "Service Item {0} is disabled."
msgstr "مورد سرویس {0} غیرفعال است."
msgstr "آیتم خدمات {0} غیرفعال است."
#: erpnext/subcontracting/doctype/subcontracting_bom/subcontracting_bom.py:67
#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:160
msgid "Service Item {0} must be a non-stock item."
msgstr "مورد سرویس {0} باید یک کالای غیر موجودی باشد."
msgstr "آیتم خدمات {0} باید یک آیتم غیر موجودی باشد."
#. Label of the service_items_section (Section Break) field in DocType
#. 'Subcontracting Order'
@@ -48192,7 +48192,7 @@ msgstr "تاریخ انتشار جدید را تنظیم کنید"
#. DocType 'Manufacturing Settings'
#: erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
msgid "Set Operating Cost / Scrap Items From Sub-assemblies"
msgstr ""
msgstr "تنظیم هزینه عملیاتی / آیتم‌های ضایعات از زیر مونتاژها"
#. Label of the set_cost_based_on_bom_qty (Check) field in DocType 'BOM
#. Operation'
@@ -48276,7 +48276,7 @@ msgstr "تنظیم انبار هدف"
#. Creator'
#: erpnext/manufacturing/doctype/bom_creator/bom_creator.json
msgid "Set Valuation Rate Based on Source Warehouse"
msgstr "نرخ ارزش گذاری را بر اساس انبار منبع تنظیم کنید"
msgstr "نرخ ارزشگذاری را بر اساس انبار منبع تنظیم کنید"
#: erpnext/selling/doctype/sales_order/sales_order.js:216
msgid "Set Warehouse"
@@ -48478,7 +48478,7 @@ msgstr "مستقر شده"
#. Option for the 'Status' (Select) field in DocType 'Workstation'
#: erpnext/manufacturing/doctype/workstation/workstation.json
msgid "Setup"
msgstr "تنظیمات"
msgstr "راه‌اندازی"
#: erpnext/public/js/setup_wizard.js:18
msgid "Setup your organization"
@@ -49301,7 +49301,7 @@ msgstr "نوع سند منبع"
#. Label of the source_exchange_rate (Float) field in DocType 'Payment Entry'
#: erpnext/accounts/doctype/payment_entry/payment_entry.json
msgid "Source Exchange Rate"
msgstr "نرخ مبادله منبع"
msgstr "نرخ تبدیل منبع"
#. Label of the source_fieldname (Data) field in DocType 'Inventory Dimension'
#: erpnext/stock/doctype/inventory_dimension/inventory_dimension.json
@@ -51680,7 +51680,7 @@ msgstr "گروه تامین کننده"
#. Name of a DocType
#: erpnext/accounts/doctype/supplier_group_item/supplier_group_item.json
msgid "Supplier Group Item"
msgstr "مورد گروه تامین کننده"
msgstr "آیتم گروه تامین کننده"
#. Label of the supplier_group_name (Data) field in DocType 'Supplier Group'
#: erpnext/setup/doctype/supplier_group/supplier_group.json
@@ -51726,7 +51726,7 @@ msgstr "فاکتور تامین کننده در فاکتور خرید وجود
#. Name of a DocType
#: erpnext/accounts/doctype/supplier_item/supplier_item.json
msgid "Supplier Item"
msgstr "مورد تامین کننده"
msgstr "آیتم تامین کننده"
#. Label of the supplier_items (Table) field in DocType 'Item'
#: erpnext/stock/doctype/item/item.json
@@ -53401,7 +53401,7 @@ msgstr "قلمرو"
#. Name of a DocType
#: erpnext/accounts/doctype/territory_item/territory_item.json
msgid "Territory Item"
msgstr "قلمرو مورد"
msgstr "آیتم قلمرو"
#. Label of the territory_manager (Link) field in DocType 'Territory'
#: erpnext/setup/doctype/territory/territory.json
@@ -54861,7 +54861,7 @@ msgstr "گنجاندن آیتم‌های غیر موجودی در برنامه
#. (Check) field in DocType 'Manufacturing Settings'
#: erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
msgid "To include sub-assembly costs and scrap items in Finished Goods on a work order without using a job card, when the 'Use Multi-Level BOM' option is enabled."
msgstr ""
msgstr "برای گنجاندن هزینه‌های زیر مونتاژ و آیتم‌های ضایعات در کالاهای نهایی در یک دستور کار بدون استفاده از کارت کار، زمانی که گزینه «استفاده از BOM چند سطحی» فعال است."
#: erpnext/accounts/doctype/payment_entry/payment_entry.py:2337
#: erpnext/controllers/accounts_controller.py:3002
@@ -56580,7 +56580,7 @@ msgstr ""
#: erpnext/setup/utils.py:137
msgid "Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually"
msgstr "نرخ مبادله {0} تا {1} برای تاریخ کلیدی {2} یافت نشد. لطفاً یک رکورد تبادل ارز به صورت دستی ایجاد کنید"
msgstr "نرخ تبدیل {0} تا {1} برای تاریخ کلیدی {2} یافت نشد. لطفاً یک رکورد تبدیل ارز به صورت دستی ایجاد کنید"
#: erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py:78
msgid "Unable to find score starting at {0}. You need to have standing scores covering 0 to 100"
@@ -56738,7 +56738,7 @@ msgstr "فاقد صلاحیت"
#. 'Company'
#: erpnext/setup/doctype/company/company.json
msgid "Unrealized Exchange Gain/Loss Account"
msgstr "حساب سود/زیان ارز تحقق نیافته"
msgstr "حساب سود/زیان تبدیل تحقق نیافته"
#. Label of the unrealized_profit_loss_account (Link) field in DocType
#. 'Purchase Invoice'
@@ -56938,7 +56938,7 @@ msgstr "به روز رسانی هزینه BOM به صورت خودکار"
#. 'Manufacturing Settings'
#: erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
msgid "Update BOM cost automatically via scheduler, based on the latest Valuation Rate/Price List Rate/Last Purchase Rate of raw materials"
msgstr "به‌روزرسانی هزینه BOM به‌طور خودکار از طریق زمان‌بندی، بر اساس آخرین نرخ ارزیابی/نرخ فهرست قیمت/آخرین نرخ خرید مواد خام"
msgstr "به‌روزرسانی هزینه BOM به‌طور خودکار از طریق زمان‌بندی، بر اساس آخرین نرخ ارزش‌گذاری/نرخ فهرست قیمت/آخرین نرخ خرید مواد خام"
#. Label of the update_billed_amount_in_delivery_note (Check) field in DocType
#. 'POS Invoice'
@@ -57256,7 +57256,7 @@ msgstr ""
#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
#: erpnext/buying/doctype/buying_settings/buying_settings.json
msgid "Use Transaction Date Exchange Rate"
msgstr "از نرخ مبادله تاریخ تراکنش استفاده کنید"
msgstr "استفاده از نرخ تبدیل تاریخ تراکنش"
#: erpnext/projects/doctype/project/project.py:560
msgid "Use a name that is different from previous project name"
@@ -57533,7 +57533,7 @@ msgstr "اعتبارسنجی قانون اعمال شده"
#. 'Manufacturing Settings'
#: erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
msgid "Validate Components and Quantities Per BOM"
msgstr ""
msgstr "اعتبارسنجی مقادیر و اجزاء در هر BOM"
#. Label of the validate_negative_stock (Check) field in DocType 'Inventory
#. Dimension'
@@ -57551,7 +57551,7 @@ msgstr ""
#. Settings'
#: erpnext/selling/doctype/selling_settings/selling_settings.json
msgid "Validate Selling Price for Item Against Purchase Rate or Valuation Rate"
msgstr "اعتبارسنجی قیمت فروش کالا در مقایسه با نرخ خرید یا نرخ ارزیابی"
msgstr "اعتبارسنجی قیمت فروش کالا در مقایسه با نرخ خرید یا نرخ ارزش‌گذاری"
#. Label of the validate_stock_on_save (Check) field in DocType 'POS Profile'
#: erpnext/accounts/doctype/pos_profile/pos_profile.json
@@ -57648,27 +57648,27 @@ msgstr "روش ارزش گذاری"
#: erpnext/stock/report/stock_balance/stock_balance.py:489
#: erpnext/stock/report/stock_ledger/stock_ledger.py:297
msgid "Valuation Rate"
msgstr "نرخ ارزش گذاری"
msgstr "نرخ ارزشگذاری"
#: erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py:166
msgid "Valuation Rate (In / Out)"
msgstr "نرخ ارزش گذاری (ورودی/خروجی)"
msgstr "نرخ ارزشگذاری (ورودی/خروجی)"
#: erpnext/stock/stock_ledger.py:1875
msgid "Valuation Rate Missing"
msgstr "نرخ ارزیابی وجود ندارد"
msgstr "نرخ ارزش‌گذاری وجود ندارد"
#: erpnext/stock/stock_ledger.py:1853
msgid "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
msgstr "نرخ ارزش گذاری برای آیتم {0}، برای انجام ثبت‌های حسابداری برای {1} {2} لازم است."
msgstr "نرخ ارزشگذاری برای آیتم {0}، برای انجام ثبت‌های حسابداری برای {1} {2} لازم است."
#: erpnext/stock/doctype/item/item.py:266
msgid "Valuation Rate is mandatory if Opening Stock entered"
msgstr "در صورت ثبت موجودی افتتاحیه، نرخ ارزش گذاری الزامی است"
msgstr "در صورت ثبت موجودی افتتاحیه، نرخ ارزشگذاری الزامی است"
#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:710
msgid "Valuation Rate required for Item {0} at row {1}"
msgstr "نرخ ارزش گذاری الزامی است برای آیتم {0} در ردیف {1}"
msgstr "نرخ ارزشگذاری الزامی است برای آیتم {0} در ردیف {1}"
#. Option for the 'Consider Tax or Charge for' (Select) field in DocType
#. 'Purchase Taxes and Charges'
@@ -57678,7 +57678,7 @@ msgstr "ارزش گذاری و کل"
#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:929
msgid "Valuation rate for customer provided items has been set to zero."
msgstr "نرخ ارزش گذاری برای آیتم‌های ارائه شده توسط مشتری صفر تعیین شده است."
msgstr "نرخ ارزشگذاری برای آیتم‌های ارائه شده توسط مشتری صفر تعیین شده است."
#. Description of the 'Sales Incoming Rate' (Currency) field in DocType
#. 'Purchase Invoice Item'
@@ -58040,7 +58040,7 @@ msgstr "مشاهده نمودار حساب ها"
#: erpnext/accounts/doctype/payment_entry/payment_entry.js:247
msgid "View Exchange Gain/Loss Journals"
msgstr "مشاهده دفترهای روزنامه سود/زیان تبادل"
msgstr "مشاهده دفترهای روزنامه سود/زیان تبدیل"
#: erpnext/assets/doctype/asset/asset.js:164
#: erpnext/assets/doctype/asset_repair/asset_repair.js:76
@@ -59120,7 +59120,7 @@ msgstr "هنگام ایجاد حساب برای شرکت فرزند {0}، حسا
#. DocType 'Buying Settings'
#: erpnext/buying/doctype/buying_settings/buying_settings.json
msgid "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."
msgstr "هنگام تهیه فاکتور خرید از سفارش خرید، به جای ارث بردن آن از سفارش خرید، از نرخ مبادله در تاریخ تراکنش فاکتور استفاده کنید. فقط برای فاکتور خرید اعمال می‌شود."
msgstr "هنگام تهیه فاکتور خرید از سفارش خرید، به جای ارث بردن آن از سفارش خرید، از نرخ تبدیل در تاریخ تراکنش فاکتور استفاده کنید. فقط برای فاکتور خرید اعمال می‌شود."
#: erpnext/setup/setup_wizard/operations/install_fixtures.py:269
msgid "White"
@@ -59257,7 +59257,7 @@ msgstr "مواد مصرفی دستور کار"
#. Name of a DocType
#: erpnext/manufacturing/doctype/work_order_item/work_order_item.json
msgid "Work Order Item"
msgstr "مورد دستور کار"
msgstr "آیتم دستور کار"
#. Name of a DocType
#: erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
@@ -60498,11 +60498,11 @@ msgstr "{0} برای حساب {1} اجباری است"
#: erpnext/public/js/controllers/taxes_and_totals.js:122
msgid "{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}"
msgstr "{0} اجباری است. شاید رکورد تبادل ارز برای {1} تا {2} ایجاد نشده باشد"
msgstr "{0} اجباری است. شاید رکورد تبدیل ارز برای {1} تا {2} ایجاد نشده باشد"
#: erpnext/controllers/accounts_controller.py:2934
msgid "{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}."
msgstr "{0} اجباری است. شاید رکورد تبادل ارز برای {1} تا {2} ایجاد نشده باشد."
msgstr "{0} اجباری است. شاید رکورد تبدیل ارز برای {1} تا {2} ایجاد نشده باشد."
#: erpnext/selling/doctype/customer/customer.py:202
msgid "{0} is not a company bank account"

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: hello@frappe.io\n"
"POT-Creation-Date: 2025-05-04 09:35+0000\n"
"PO-Revision-Date: 2025-05-05 10:00\n"
"PO-Revision-Date: 2025-05-06 17:06\n"
"Last-Translator: hello@frappe.io\n"
"Language-Team: Swedish\n"
"MIME-Version: 1.0\n"
@@ -19574,7 +19574,7 @@ msgstr "Befintlig Kund"
#. Label of the exit (Tab Break) field in DocType 'Employee'
#: erpnext/setup/doctype/employee/employee.json
msgid "Exit"
msgstr "Avgång"
msgstr "Avsluta"
#: erpnext/selling/page/point_of_sale/pos_controller.js:254
msgid "Exit Full Screen"
@@ -19583,7 +19583,7 @@ msgstr "Avsluta Helskärm"
#. Label of the held_on (Date) field in DocType 'Employee'
#: erpnext/setup/doctype/employee/employee.json
msgid "Exit Interview Held On"
msgstr "Avgång Intervju"
msgstr "Avgång Intervju Datum"
#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:154
#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:187

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: hello@frappe.io\n"
"POT-Creation-Date: 2025-05-04 09:35+0000\n"
"PO-Revision-Date: 2025-05-05 17:02\n"
"PO-Revision-Date: 2025-05-06 17:06\n"
"Last-Translator: hello@frappe.io\n"
"Language-Team: Turkish\n"
"MIME-Version: 1.0\n"
@@ -146,7 +146,7 @@ msgstr "% Bitmiş Ürün Miktarı"
#. Label of the per_installed (Percent) field in DocType 'Delivery Note'
#: erpnext/stock/doctype/delivery_note/delivery_note.json
msgid "% Installed"
msgstr "% Kuruldu"
msgstr "% Tamamlandı"
#: erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js:70
#: erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html:16

View File

@@ -7,7 +7,7 @@ from collections import deque
from operator import itemgetter
import frappe
from frappe import _
from frappe import _, bold
from frappe.core.doctype.version.version import get_diff
from frappe.model.mapper import get_mapped_doc
from frappe.utils import cint, cstr, flt, parse_json, today
@@ -662,9 +662,16 @@ class BOM(WebsiteGenerator):
def check_recursion(self, bom_list=None):
"""Check whether recursion occurs in any bom"""
def _throw_error(bom_name):
def _throw_error(bom_name, production_item=None):
msg = _("BOM recursion: {1} cannot be parent or child of {0}").format(self.name, bom_name)
if production_item and bom_name != self.name:
msg += "<br><br>"
msg += _(
"Note: If you want to use the finished good {0} as a raw material, then enable the 'Do Not Explode' checkbox in the Items table against the same raw material."
).format(bold(production_item))
frappe.throw(
_("BOM recursion: {1} cannot be parent or child of {0}").format(self.name, bom_name),
msg,
exc=BOMRecursionError,
)
@@ -681,7 +688,7 @@ class BOM(WebsiteGenerator):
if self.item == item.item_code and item.bom_no:
# Same item but with different BOM should not be allowed.
# Same item can appear recursively once as long as it doesn't have BOM.
_throw_error(item.bom_no)
_throw_error(item.bom_no, self.item)
if self.name in {d.bom_no for d in self.items}:
_throw_error(self.name)

View File

@@ -10,13 +10,17 @@
"warehouse",
"item_name",
"material_request_type",
"quantity",
"required_bom_qty",
"column_break_4",
"schedule_date",
"uom",
"conversion_factor",
"section_break_azee",
"required_bom_qty",
"projected_qty",
"column_break_wack",
"quantity",
"stock_reserved_qty",
"item_details",
"schedule_date",
"description",
"min_order_qty",
"section_break_8",
@@ -27,7 +31,6 @@
"reserved_qty_for_production",
"column_break_yhelv",
"ordered_qty",
"projected_qty",
"safety_stock"
],
"fields": [
@@ -47,7 +50,7 @@
"label": "Item Name"
},
{
"columns": 2,
"columns": 3,
"fieldname": "warehouse",
"fieldtype": "Link",
"in_list_view": 1,
@@ -58,7 +61,7 @@
"search_index": 1
},
{
"columns": 1,
"columns": 2,
"fieldname": "material_request_type",
"fieldtype": "Select",
"in_list_view": 1,
@@ -70,17 +73,19 @@
"fieldtype": "Column Break"
},
{
"columns": 2,
"columns": 3,
"fieldname": "quantity",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Plan to Request Qty",
"label": "Required Qty",
"no_copy": 1,
"reqd": 1
},
{
"columns": 2,
"fieldname": "projected_qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Projected Qty",
"read_only": 1
},
@@ -89,7 +94,6 @@
"default": "0",
"fieldname": "actual_qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Qty In Stock",
"no_copy": 1,
"read_only": 1
@@ -172,11 +176,11 @@
"read_only": 1
},
{
"columns": 2,
"columns": 3,
"fieldname": "required_bom_qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Qty As Per BOM",
"label": "Reqd Qty (BOM)",
"no_copy": 1,
"read_only": 1
},
@@ -187,7 +191,7 @@
"read_only": 1
},
{
"columns": 1,
"columns": 2,
"fieldname": "schedule_date",
"fieldtype": "Date",
"in_list_view": 1,
@@ -201,19 +205,36 @@
{
"fieldname": "column_break_yhelv",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_azee",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_wack",
"fieldtype": "Column Break"
},
{
"fieldname": "stock_reserved_qty",
"fieldtype": "Float",
"label": "Stock Reserved Qty",
"no_copy": 1,
"read_only": 1
}
],
"grid_page_length": 50,
"istable": 1,
"links": [],
"modified": "2024-12-30 18:06:22.288340",
"modified": "2025-05-01 14:50:55.805442",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Material Request Plan Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -42,6 +42,7 @@ class MaterialRequestPlanItem(Document):
safety_stock: DF.Float
sales_order: DF.Link | None
schedule_date: DF.Date | None
stock_reserved_qty: DF.Float
uom: DF.Link | None
warehouse: DF.Link
# end: auto-generated types

View File

@@ -9,6 +9,13 @@ frappe.ui.form.on("Production Plan", {
});
},
hide_reserve_stock_button(frm) {
frm.toggle_display("reserve_stock", false);
if (frm.doc.__onload?.enable_stock_reservation) {
frm.toggle_display("reserve_stock", true);
}
},
setup(frm) {
frm.trigger("setup_queries");
@@ -16,6 +23,9 @@ frappe.ui.form.on("Production Plan", {
"Work Order": "Work Order / Subcontract PO",
"Material Request": "Material Request",
};
frm.set_df_property("sub_assembly_items", "cannot_delete_rows", true);
frm.set_df_property("mr_items", "cannot_delete_rows", true);
},
setup_queries(frm) {
@@ -140,12 +150,16 @@ frappe.ui.form.on("Production Plan", {
);
}
}
if (frm.doc.status !== "Closed") {
frm.page.set_inner_btn_group_as_primary(__("Create"));
}
}
if (frm.doc.status !== "Closed") {
frm.page.set_inner_btn_group_as_primary(__("Create"));
}
frm.trigger("material_requirement");
frm.trigger("hide_reserve_stock_button");
frm.trigger("setup_stock_reservation_for_sub_assembly");
frm.trigger("setup_stock_reservation_for_raw_materials");
const projected_qty_formula = ` <table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
<tr><td style="padding-left:25px">
@@ -193,6 +207,72 @@ frappe.ui.form.on("Production Plan", {
set_field_options("projected_qty_formula", projected_qty_formula);
},
has_unreserved_stock(frm, table) {
let has_unreserved_stock = frm.doc[table].some(
(item) => flt(item.qty) > flt(item.stock_reserved_qty)
);
return has_unreserved_stock;
},
has_reserved_stock(frm, table) {
let has_reserved_stock = frm.doc[table].some((item) => flt(item.stock_reserved_qty) > 0);
return has_reserved_stock;
},
setup_stock_reservation_for_sub_assembly(frm) {
if (frm.doc.docstatus === 1 && frm.doc.reserve_stock) {
if (frm.events.has_unreserved_stock(frm, "sub_assembly_items")) {
frm.add_custom_button(
__("Reserve for Sub-assembly"),
() => erpnext.stock_reservation.make_entries(frm, "sub_assembly_items"),
__("Stock Reservation")
);
}
if (frm.events.has_reserved_stock(frm, "sub_assembly_items")) {
frm.add_custom_button(
__("Unreserve for Sub-assembly"),
() => erpnext.stock_reservation.unreserve_stock(frm),
__("Stock Reservation")
);
frm.add_custom_button(
__("Reserved Stock for Sub-assembly"),
() => erpnext.stock_reservation.show_reserved_stock(frm, "sub_assembly_items"),
__("Stock Reservation")
);
}
}
},
setup_stock_reservation_for_raw_materials(frm) {
if (frm.doc.docstatus === 1 && frm.doc.reserve_stock) {
if (frm.events.has_unreserved_stock(frm, "mr_items")) {
frm.add_custom_button(
__("Reserve for Raw Materials"),
() => erpnext.stock_reservation.make_entries(frm, "mr_items"),
__("Stock Reservation")
);
}
if (frm.events.has_reserved_stock(frm, "mr_items")) {
frm.add_custom_button(
__("Unreserve for Raw Materials"),
() => erpnext.stock_reservation.unreserve_stock(frm),
__("Stock Reservation")
);
frm.add_custom_button(
__("Reserved Stock for Raw Materials"),
() => erpnext.stock_reservation.show_reserved_stock(frm, "mr_items"),
__("Stock Reservation")
);
}
}
},
close_open_production_plan(frm, close = false) {
frappe.call({
method: "set_status",

View File

@@ -11,6 +11,7 @@
"get_items_from",
"column_break1",
"posting_date",
"reserve_stock",
"filters",
"item_code",
"customer",
@@ -23,6 +24,7 @@
"from_delivery_date",
"to_delivery_date",
"sales_orders_detail",
"combine_items",
"get_sales_orders",
"sales_orders",
"material_request_detail",
@@ -30,16 +32,14 @@
"material_requests",
"select_items_to_manufacture_section",
"get_items",
"combine_items",
"po_items",
"section_break_25",
"prod_plan_references",
"section_break_24",
"combine_sub_items",
"sub_assembly_warehouse",
"section_break_ucc4",
"skip_available_sub_assembly_item",
"column_break_igxl",
"skip_available_sub_assembly_item",
"combine_sub_items",
"get_sub_assembly_items",
"section_break_g4ip",
"sub_assembly_items",
@@ -215,7 +215,8 @@
{
"fieldname": "material_request_planning",
"fieldtype": "Section Break",
"label": "Material Request Planning"
"hide_border": 1,
"label": "For Raw Materials"
},
{
"default": "1",
@@ -231,10 +232,10 @@
},
{
"default": "1",
"description": "If enabled, the system will consider items with a shortfall in quantity. \n<br>\nQty = Reqd Qty (BOM) - <a href=\"https://docs.frappe.io/erpnext/user/manual/en/projected-quantity\">Projected Qty</a>",
"description": "If enabled, formula for <b>Required Qty</b>: <br>\nRequired Qty (BOM) - <a href=\"https://docs.frappe.io/erpnext/user/manual/en/projected-quantity\">Projected Qty</a>. <br> This helps avoid over-ordering.",
"fieldname": "ignore_existing_ordered_qty",
"fieldtype": "Check",
"label": "Skip Available Raw Materials"
"label": "Consider Projected Qty in Calculation (RM)"
},
{
"fieldname": "column_break_25",
@@ -314,7 +315,7 @@
{
"fieldname": "for_warehouse",
"fieldtype": "Link",
"label": "Raw Materials Warehouse",
"label": "For Warehouse",
"options": "Warehouse"
},
{
@@ -375,11 +376,13 @@
"label": "Get Sub Assembly Items"
},
{
"depends_on": "eval:doc.get_items_from == 'Sales Order'",
"fieldname": "from_delivery_date",
"fieldtype": "Date",
"label": "From Delivery Date"
},
{
"depends_on": "eval:doc.get_items_from == 'Sales Order'",
"fieldname": "to_delivery_date",
"fieldtype": "Date",
"label": "To Delivery Date"
@@ -404,15 +407,10 @@
},
{
"default": "1",
"description": "If enabled, the system will consider items with a shortfall in quantity. \n<br>\nQty = Reqd Qty (BOM) - <a href=\"https://docs.frappe.io/erpnext/user/manual/en/projected-quantity\">Projected Qty</a>",
"description": "If enabled, formula for <b>Qty to Order</b>: <br>\nRequired Qty (BOM) - <a href=\"https://docs.frappe.io/erpnext/user/manual/en/projected-quantity\">Projected Qty</a>. <br> This helps avoid over-ordering.",
"fieldname": "skip_available_sub_assembly_item",
"fieldtype": "Check",
"label": "Skip Available Sub Assembly Items"
},
{
"fieldname": "section_break_ucc4",
"fieldtype": "Column Break",
"hide_border": 1
"label": "Consider Projected Qty in Calculation"
},
{
"fieldname": "section_break_g4ip",
@@ -423,7 +421,7 @@
"fieldtype": "Column Break"
},
{
"description": "When a parent warehouse is chosen, the system conducts stock checks against the associated child warehouses",
"description": "When a parent warehouse is chosen, the system conducts Project Qty checks against the associated child warehouses",
"fieldname": "sub_assembly_warehouse",
"fieldtype": "Link",
"label": "Sub Assembly Warehouse",
@@ -435,6 +433,12 @@
"fieldname": "consider_minimum_order_qty",
"fieldtype": "Check",
"label": "Consider Minimum Order Qty"
},
{
"default": "0",
"fieldname": "reserve_stock",
"fieldtype": "Check",
"label": "Reserve Stock"
}
],
"grid_page_length": 50,
@@ -442,7 +446,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-04-08 17:24:09.394056",
"modified": "2025-05-09 18:55:45.500257",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",

View File

@@ -9,6 +9,8 @@ from collections import defaultdict
import frappe
from frappe import _, msgprint
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import Case
from frappe.query_builder.functions import IfNull, Sum
from frappe.utils import (
add_days,
@@ -20,6 +22,7 @@ from frappe.utils import (
getdate,
now_datetime,
nowdate,
parse_json,
)
from frappe.utils.csvutils import build_csv_response
from pypika.terms import ExistsCriterion
@@ -28,6 +31,7 @@ from erpnext.manufacturing.doctype.bom.bom import get_children as get_bom_childr
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import StockReservation
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.stock.utils import get_or_make_bin
from erpnext.utilities.transaction_base import validate_uom_is_integer
@@ -84,6 +88,7 @@ class ProductionPlan(Document):
posting_date: DF.Date
prod_plan_references: DF.Table[ProductionPlanItemReference]
project: DF.Link | None
reserve_stock: DF.Check
sales_order_status: DF.Literal["", "To Deliver and Bill", "To Bill", "To Deliver"]
sales_orders: DF.Table[ProductionPlanSalesOrder]
skip_available_sub_assembly_item: DF.Check
@@ -108,6 +113,12 @@ class ProductionPlan(Document):
warehouses: DF.TableMultiSelect[ProductionPlanMaterialRequestWarehouse]
# end: auto-generated types
def onload(self):
self.set_onload(
"enable_stock_reservation",
frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"),
)
def validate(self):
self.set_pending_qty_in_row_without_reference()
self.calculate_total_planned_qty()
@@ -116,6 +127,11 @@ class ProductionPlan(Document):
validate_uom_is_integer(self, "stock_uom", "planned_qty")
self.validate_sales_orders()
self.validate_material_request_type()
self.enable_auto_reserve_stock()
def enable_auto_reserve_stock(self):
if self.is_new() and frappe.db.get_single_value("Stock Settings", "auto_reserve_stock"):
self.reserve_stock = 1
def validate_material_request_type(self):
for row in self.get("mr_items"):
@@ -552,12 +568,20 @@ class ProductionPlan(Document):
def on_submit(self):
self.update_bin_qty()
self.update_sales_order()
self.update_stock_reservation()
def on_cancel(self):
self.db_set("status", "Cancelled")
self.delete_draft_work_order()
self.update_bin_qty()
self.update_sales_order()
self.update_stock_reservation()
def update_stock_reservation(self):
if not self.reserve_stock:
return
make_stock_reservation_entries(self)
def update_sales_order(self):
sales_orders = [row.sales_order for row in self.po_items if row.sales_order]
@@ -851,6 +875,7 @@ class ProductionPlan(Document):
wo = frappe.new_doc("Work Order")
wo.update(item)
wo.reserve_stock = self.reserve_stock
wo.planned_start_date = item.get("planned_start_date") or item.get("schedule_date")
if item.get("warehouse"):
@@ -1369,29 +1394,28 @@ def get_material_request_items(
get_conversion_factor(row.item_code, item_details.purchase_uom).get("conversion_factor") or 1.0
)
if required_qty > 0:
return {
"item_code": row.item_code,
"item_name": row.item_name,
"quantity": required_qty / conversion_factor,
"conversion_factor": conversion_factor,
"required_bom_qty": total_qty,
"stock_uom": row.get("stock_uom"),
"warehouse": warehouse
or row.get("source_warehouse")
or row.get("default_warehouse")
or item_group_defaults.get("default_warehouse"),
"safety_stock": row.safety_stock,
"actual_qty": bin_dict.get("actual_qty", 0),
"projected_qty": bin_dict.get("projected_qty", 0),
"ordered_qty": bin_dict.get("ordered_qty", 0),
"reserved_qty_for_production": bin_dict.get("reserved_qty_for_production", 0),
"min_order_qty": row["min_order_qty"],
"material_request_type": row.get("default_material_request_type"),
"sales_order": sales_order,
"description": row.get("description"),
"uom": row.get("purchase_uom") or row.get("stock_uom"),
}
return {
"item_code": row.item_code,
"item_name": row.item_name,
"quantity": required_qty / conversion_factor,
"conversion_factor": conversion_factor,
"required_bom_qty": total_qty,
"stock_uom": row.get("stock_uom"),
"warehouse": warehouse
or row.get("source_warehouse")
or row.get("default_warehouse")
or item_group_defaults.get("default_warehouse"),
"safety_stock": row.safety_stock,
"actual_qty": bin_dict.get("actual_qty", 0),
"projected_qty": bin_dict.get("projected_qty", 0),
"ordered_qty": bin_dict.get("ordered_qty", 0),
"reserved_qty_for_production": bin_dict.get("reserved_qty_for_production", 0),
"min_order_qty": row["min_order_qty"],
"material_request_type": row.get("default_material_request_type"),
"sales_order": sales_order,
"description": row.get("description"),
"uom": row.get("purchase_uom") or row.get("stock_uom"),
}
def get_sales_orders(self):
@@ -1792,6 +1816,7 @@ def get_sub_assembly_items(
if d.expandable:
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
required_qty = stock_qty
if skip_available_sub_assembly_item and d.item_code not in sub_assembly_items:
bin_details.setdefault(d.item_code, get_bin_details(d, company, for_warehouse=warehouse))
@@ -1808,40 +1833,43 @@ def get_sub_assembly_items(
elif warehouse:
bin_details.setdefault(d.item_code, get_bin_details(d, company, for_warehouse=warehouse))
if stock_qty > 0:
bom_data.append(
frappe._dict(
{
"actual_qty": bin_details[d.item_code][0].get("actual_qty", 0)
if bin_details.get(d.item_code)
else 0,
"parent_item_code": parent_item_code,
"description": d.description,
"production_item": d.item_code,
"item_name": d.item_name,
"stock_uom": d.stock_uom,
"uom": d.stock_uom,
"bom_no": d.value,
"is_sub_contracted_item": d.is_sub_contracted_item,
"bom_level": indent,
"indent": indent,
"stock_qty": stock_qty,
}
)
bom_data.append(
frappe._dict(
{
"actual_qty": bin_details[d.item_code][0].get("actual_qty", 0)
if bin_details.get(d.item_code)
else 0,
"parent_item_code": parent_item_code,
"description": d.description,
"production_item": d.item_code,
"item_name": d.item_name,
"stock_uom": d.stock_uom,
"uom": d.stock_uom,
"bom_no": d.value,
"is_sub_contracted_item": d.is_sub_contracted_item,
"bom_level": indent,
"indent": indent,
"stock_qty": stock_qty,
"required_qty": required_qty,
"projected_qty": bin_details[d.item_code][0].get("projected_qty", 0)
if bin_details.get(d.item_code)
else 0,
}
)
)
if d.value:
get_sub_assembly_items(
sub_assembly_items,
bin_details,
d.value,
bom_data,
stock_qty,
company,
warehouse,
indent=indent + 1,
skip_available_sub_assembly_item=skip_available_sub_assembly_item,
)
if d.value:
get_sub_assembly_items(
sub_assembly_items,
bin_details,
d.value,
bom_data,
stock_qty,
company,
warehouse,
indent=indent + 1,
skip_available_sub_assembly_item=skip_available_sub_assembly_item,
)
def set_default_warehouses(row, default_warehouses):
@@ -2034,7 +2062,12 @@ def get_reserved_qty_for_sub_assembly(item_code, warehouse):
frappe.qb.from_(table)
.inner_join(child)
.on(table.name == child.parent)
.select(Sum(child.qty - IfNull(child.wo_produced_qty, 0)))
.select(
Sum(
Case().when(child.qty > 0, child.qty).else_(child.required_qty)
- IfNull(child.wo_produced_qty, 0)
)
)
.where(
(table.docstatus == 1)
& (child.production_item == item_code)
@@ -2050,3 +2083,39 @@ def get_reserved_qty_for_sub_assembly(item_code, warehouse):
qty = flt(query[0][0])
return qty if qty > 0 else 0.0
@frappe.whitelist()
def make_stock_reservation_entries(doc, items=None, table_name=None, notify=False):
if isinstance(doc, str):
doc = parse_json(doc)
doc = frappe.get_doc("Work Order", doc.get("name"))
if items and isinstance(items, str):
items = parse_json(items)
mapper = {
"sub_assembly_items": {
"table_name": "sub_assembly_items",
"qty_field": "required_qty",
"warehouse_field": "fg_warehouse",
},
"mr_items": {
"table_name": "mr_items",
"qty_field": "required_bom_qty",
"warehouse_field": "warehouse",
},
}
for child_table_name in mapper:
if table_name and table_name != child_table_name:
continue
sre = StockReservation(doc, items=items, kwargs=mapper[child_table_name], notify=notify)
if doc.docstatus == 1:
sre.make_stock_reservation_entries()
frappe.msgprint(_("Stock Reservation Entries Created"), alert=True)
elif doc.docstatus == 2:
sre.cancel_stock_reservation_entries()
doc.reload()

View File

@@ -4,8 +4,12 @@ from frappe import _
def get_data():
return {
"fieldname": "production_plan",
"non_standard_fieldnames": {
"Stock Reservation Entry": "voucher_no",
},
"transactions": [
{"label": _("Transactions"), "items": ["Work Order", "Material Request"]},
{"label": _("Subcontract"), "items": ["Purchase Order"]},
{"label": _("Reservation"), "items": ["Stock Reservation Entry"]},
],
}

View File

@@ -16,10 +16,15 @@ from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import create_item, make_item
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_batch_from_bundle,
get_serial_nos_from_bundle,
)
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import StockReservation
class UnitTestProductionPlan(UnitTestCase):
@@ -146,7 +151,7 @@ class TestProductionPlan(IntegrationTestCase):
item_code="Raw Material Item 2", target="_Test Warehouse - _TC", qty=1, rate=120
)
pln = create_production_plan(item_code="Test Production Item 1", ignore_existing_ordered_qty=1)
pln = create_production_plan(item_code="Test Production Item 1", ignore_existing_ordered_qty=0)
self.assertTrue(len(pln.mr_items))
self.assertTrue(flt(pln.mr_items[0].quantity), 1.0)
@@ -182,11 +187,17 @@ class TestProductionPlan(IntegrationTestCase):
pln = create_production_plan(
item_code="Test Production Item 1", use_multi_level_bom=0, ignore_existing_ordered_qty=1
)
self.assertFalse(len(pln.mr_items))
items = []
for row in pln.mr_items:
if row.quantity > 0:
items.append(row.item_code)
self.assertFalse(len(items))
pln.cancel()
sr1.cancel()
sr2.cancel()
pln.cancel()
def test_production_plan_sales_orders(self):
"Test if previously fulfilled SO (with WO) is pulled into Prod Plan."
@@ -1910,6 +1921,417 @@ class TestProductionPlan(IntegrationTestCase):
self.assertEqual(mr_items[0].get("quantity"), 80)
self.assertEqual(mr_items[1].get("quantity"), 70)
def test_stock_reservation_against_production_plan(self):
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1)
bom_tree = {
"Finished Good For SR": {
"Sub Assembly For SR 1": {"Raw Material For SR 1": {}},
"Sub Assembly For SR 2": {"Raw Material For SR 2": {}},
"Sub Assembly For SR 3": {"Raw Material For SR 3": {}},
}
}
parent_bom = create_nested_bom(bom_tree, prefix="")
warehouse = "_Test Warehouse - _TC"
for item_code in [
"Sub Assembly For SR 1",
"Sub Assembly For SR 2",
"Sub Assembly For SR 3",
"Raw Material For SR 1",
"Raw Material For SR 2",
"Raw Material For SR 3",
]:
make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100)
plan = create_production_plan(
item_code=parent_bom.item,
planned_qty=15,
skip_available_sub_assembly_item=1,
ignore_existing_ordered_qty=1,
do_not_submit=1,
warehouse=warehouse,
sub_assembly_warehouse=warehouse,
for_warehouse=warehouse,
reserve_stock=1,
)
plan.get_sub_assembly_items()
plan.set("mr_items", [])
mr_items = get_items_for_material_requests(plan.as_dict())
for d in mr_items:
plan.append("mr_items", d)
plan.save()
self.assertTrue(len(plan.sub_assembly_items) == 3)
for row in plan.sub_assembly_items:
self.assertEqual(row.required_qty, 15.0)
self.assertEqual(row.qty, 10.0)
self.assertTrue(len(plan.mr_items) == 3)
for row in plan.mr_items:
self.assertEqual(row.required_bom_qty, 10.0)
self.assertEqual(row.quantity, 5.0)
plan.submit()
sre = StockReservation(plan)
reserved_entries = sre.get_reserved_entries("Production Plan", plan.name)
self.assertTrue(len(reserved_entries) == 6)
for row in reserved_entries:
self.assertEqual(row.reserved_qty, 5.0)
plan.submit_material_request = 1
plan.make_material_request()
plan.make_work_order()
material_requests = frappe.get_all(
"Material Request", filters={"production_plan": plan.name}, pluck="name"
)
self.assertTrue(len(material_requests) > 0)
for mr_name in list(set(material_requests)):
po = make_purchase_order(mr_name)
po.supplier = "_Test Supplier"
po.submit()
pr = make_purchase_receipt(po.name)
pr.submit()
sre = StockReservation(plan)
reserved_entries = sre.get_reserved_entries("Production Plan", plan.name)
self.assertTrue(len(reserved_entries) == 9)
work_orders = frappe.get_all("Work Order", filters={"production_plan": plan.name}, pluck="name")
for wo_name in list(set(work_orders)):
wo_doc = frappe.get_doc("Work Order", wo_name)
self.assertEqual(wo_doc.reserve_stock, 1)
wo_doc.source_warehouse = warehouse
wo_doc.wip_warehouse = warehouse
wo_doc.fg_warehouse = warehouse
wo_doc.submit()
sre = StockReservation(wo_doc)
reserved_entries = sre.get_reserved_entries("Work Order", wo_doc.name)
if wo_doc.production_item == "Finished Good For SR":
self.assertEqual(len(reserved_entries), 3)
else:
# For raw materials 2 stock reservation entries
# 5 qty was present already in stock and 5 added from new PO
self.assertEqual(len(reserved_entries), 2)
sre = StockReservation(plan)
reserved_entries = sre.get_reserved_entries("Production Plan", plan.name)
self.assertTrue(len(reserved_entries) == 0)
frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0)
def test_stock_reservation_of_serial_nos_against_production_plan(self):
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1)
bom_tree = {
"Finished Good For SR": {
"SN Sub Assembly For SR 1": {"SN Raw Material For SR 1": {}},
"SN Sub Assembly For SR 2": {"SN Raw Material For SR 2": {}},
"SN Sub Assembly For SR 3": {"SN Raw Material For SR 3": {}},
}
}
parent_bom = create_nested_bom(bom_tree, prefix="")
warehouse = "_Test Warehouse - _TC"
for item_code in [
"SN Sub Assembly For SR 1",
"SN Sub Assembly For SR 2",
"SN Sub Assembly For SR 3",
"SN Raw Material For SR 1",
"SN Raw Material For SR 2",
"SN Raw Material For SR 3",
]:
doc = frappe.get_doc("Item", item_code)
doc.has_serial_no = 1
doc.serial_no_series = f"SNN-{item_code}.-.#####"
doc.save()
make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100)
plan = create_production_plan(
item_code=parent_bom.item,
planned_qty=15,
skip_available_sub_assembly_item=1,
ignore_existing_ordered_qty=1,
do_not_submit=1,
warehouse=warehouse,
sub_assembly_warehouse=warehouse,
for_warehouse=warehouse,
reserve_stock=1,
)
plan.get_sub_assembly_items()
plan.set("mr_items", [])
mr_items = get_items_for_material_requests(plan.as_dict())
for d in mr_items:
plan.append("mr_items", d)
plan.save()
self.assertTrue(len(plan.sub_assembly_items) == 3)
for row in plan.sub_assembly_items:
self.assertEqual(row.required_qty, 15.0)
self.assertEqual(row.qty, 10.0)
self.assertTrue(len(plan.mr_items) == 3)
for row in plan.mr_items:
self.assertEqual(row.required_bom_qty, 10.0)
self.assertEqual(row.quantity, 5.0)
plan.submit()
sre = StockReservation(plan)
reserved_entries = sre.get_reserved_entries("Production Plan", plan.name)
self.assertTrue(len(reserved_entries) == 6)
for row in reserved_entries:
self.assertEqual(row.reserved_qty, 5.0)
plan.submit_material_request = 1
plan.make_material_request()
plan.make_work_order()
material_requests = frappe.get_all(
"Material Request", filters={"production_plan": plan.name}, pluck="name"
)
additional_serial_nos = []
for item_code in [
"SN Sub Assembly For SR 1",
"SN Sub Assembly For SR 2",
"SN Sub Assembly For SR 3",
"SN Raw Material For SR 1",
"SN Raw Material For SR 2",
"SN Raw Material For SR 3",
]:
se = make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100)
additional_serial_nos.extend(get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle))
self.assertTrue(additional_serial_nos)
self.assertTrue(len(material_requests) > 0)
for mr_name in list(set(material_requests)):
po = make_purchase_order(mr_name)
po.supplier = "_Test Supplier"
po.submit()
pr = make_purchase_receipt(po.name)
pr.submit()
sre = StockReservation(plan)
reserved_entries = sre.get_reserved_entries("Production Plan", plan.name)
self.assertTrue(len(reserved_entries) == 9)
serial_nos_res_for_pp = frappe.get_all(
"Serial and Batch Entry",
filters={"parent": ("in", [x.name for x in reserved_entries]), "docstatus": 1},
pluck="serial_no",
)
work_orders = frappe.get_all("Work Order", filters={"production_plan": plan.name}, pluck="name")
for wo_name in list(set(work_orders)):
wo_doc = frappe.get_doc("Work Order", wo_name)
self.assertEqual(wo_doc.reserve_stock, 1)
wo_doc.source_warehouse = warehouse
wo_doc.wip_warehouse = warehouse
wo_doc.fg_warehouse = warehouse
wo_doc.submit()
sre = StockReservation(wo_doc)
reserved_entries = sre.get_reserved_entries("Work Order", wo_doc.name)
serial_nos_res_for_wo = frappe.get_all(
"Serial and Batch Entry",
filters={"parent": ("in", [x.name for x in reserved_entries]), "docstatus": 1},
pluck="serial_no",
)
for serial_no in serial_nos_res_for_wo:
self.assertTrue(serial_no in serial_nos_res_for_pp)
self.assertFalse(serial_no in additional_serial_nos)
if wo_doc.production_item == "Finished Good For SR":
self.assertEqual(len(reserved_entries), 3)
else:
# For raw materials 2 stock reservation entries
# 5 qty was present already in stock and 5 added from new PO
self.assertEqual(len(reserved_entries), 2)
sre = StockReservation(plan)
reserved_entries = sre.get_reserved_entries("Production Plan", plan.name)
self.assertTrue(len(reserved_entries) == 0)
frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0)
def test_stock_reservation_of_batch_nos_against_production_plan(self):
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1)
bom_tree = {
"Finished Good For SR": {
"Batch Sub Assembly For SR 1": {"Batch Raw Material For SR 1": {}},
"Batch Sub Assembly For SR 2": {"Batch Raw Material For SR 2": {}},
"Batch Sub Assembly For SR 3": {"Batch Raw Material For SR 3": {}},
}
}
parent_bom = create_nested_bom(bom_tree, prefix="")
warehouse = "_Test Warehouse - _TC"
for item_code in [
"Batch Sub Assembly For SR 1",
"Batch Sub Assembly For SR 2",
"Batch Sub Assembly For SR 3",
"Batch Raw Material For SR 1",
"Batch Raw Material For SR 2",
"Batch Raw Material For SR 3",
]:
doc = frappe.get_doc("Item", item_code)
doc.has_batch_no = 1
doc.create_new_batch = 1
doc.batch_number_series = f"BCH-{item_code}.-.#####"
doc.save()
make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100)
plan = create_production_plan(
item_code=parent_bom.item,
planned_qty=15,
skip_available_sub_assembly_item=1,
ignore_existing_ordered_qty=1,
do_not_submit=1,
warehouse=warehouse,
sub_assembly_warehouse=warehouse,
for_warehouse=warehouse,
reserve_stock=1,
)
plan.get_sub_assembly_items()
plan.set("mr_items", [])
mr_items = get_items_for_material_requests(plan.as_dict())
for d in mr_items:
plan.append("mr_items", d)
plan.save()
self.assertTrue(len(plan.sub_assembly_items) == 3)
for row in plan.sub_assembly_items:
self.assertEqual(row.required_qty, 15.0)
self.assertEqual(row.qty, 10.0)
self.assertTrue(len(plan.mr_items) == 3)
for row in plan.mr_items:
self.assertEqual(row.required_bom_qty, 10.0)
self.assertEqual(row.quantity, 5.0)
plan.submit()
sre = StockReservation(plan)
reserved_entries = sre.get_reserved_entries("Production Plan", plan.name)
self.assertTrue(len(reserved_entries) == 6)
for row in reserved_entries:
self.assertEqual(row.reserved_qty, 5.0)
plan.submit_material_request = 1
plan.make_material_request()
plan.make_work_order()
material_requests = frappe.get_all(
"Material Request", filters={"production_plan": plan.name}, pluck="name"
)
additional_batches = []
for item_code in [
"Batch Sub Assembly For SR 1",
"Batch Sub Assembly For SR 2",
"Batch Sub Assembly For SR 3",
"Batch Raw Material For SR 1",
"Batch Raw Material For SR 2",
"Batch Raw Material For SR 3",
]:
se = make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100)
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
additional_batches.append(batch_no)
self.assertTrue(additional_batches)
self.assertTrue(len(material_requests) > 0)
for mr_name in list(set(material_requests)):
po = make_purchase_order(mr_name)
po.supplier = "_Test Supplier"
po.submit()
pr = make_purchase_receipt(po.name)
pr.submit()
sre = StockReservation(plan)
reserved_entries = sre.get_reserved_entries("Production Plan", plan.name)
self.assertTrue(len(reserved_entries) == 9)
batches_reserved_for_pp = frappe.get_all(
"Serial and Batch Entry",
filters={"parent": ("in", [x.name for x in reserved_entries]), "docstatus": 1},
pluck="batch_no",
)
work_orders = frappe.get_all("Work Order", filters={"production_plan": plan.name}, pluck="name")
for wo_name in list(set(work_orders)):
wo_doc = frappe.get_doc("Work Order", wo_name)
self.assertEqual(wo_doc.reserve_stock, 1)
wo_doc.source_warehouse = warehouse
wo_doc.wip_warehouse = warehouse
wo_doc.fg_warehouse = warehouse
wo_doc.submit()
sre = StockReservation(wo_doc)
reserved_entries = sre.get_reserved_entries("Work Order", wo_doc.name)
batches_reserved_for_wo = frappe.get_all(
"Serial and Batch Entry",
filters={"parent": ("in", [x.name for x in reserved_entries]), "docstatus": 1},
pluck="batch_no",
)
for batch_no in batches_reserved_for_wo:
self.assertTrue(batch_no in batches_reserved_for_pp)
self.assertFalse(batch_no in additional_batches)
if wo_doc.production_item == "Finished Good For SR":
self.assertEqual(len(reserved_entries), 3)
else:
# For raw materials 2 stock reservation entries
# 5 qty was present already in stock and 5 added from new PO
self.assertEqual(len(reserved_entries), 2)
sre = StockReservation(plan)
reserved_entries = sre.get_reserved_entries("Production Plan", plan.name)
self.assertTrue(len(reserved_entries) == 0)
frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0)
def create_production_plan(**args):
"""
@@ -1931,6 +2353,7 @@ def create_production_plan(**args):
"get_items_from": "Sales Order",
"skip_available_sub_assembly_item": args.skip_available_sub_assembly_item or 0,
"sub_assembly_warehouse": args.sub_assembly_warehouse,
"reserve_stock": args.reserve_stock or 0,
}
)

View File

@@ -26,7 +26,7 @@
},
{
"fieldname": "qty",
"fieldtype": "Data",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Qty"
},
@@ -37,17 +37,19 @@
"label": "Item Reference"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-03-27 13:10:20.410593",
"modified": "2025-05-07 17:47:36.244083",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Item Reference",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -19,7 +19,7 @@ class ProductionPlanItemReference(Document):
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
qty: DF.Data | None
qty: DF.Float
sales_order: DF.Link | None
sales_order_item: DF.Data | None
# end: auto-generated types

View File

@@ -9,29 +9,32 @@
"item_name",
"fg_warehouse",
"parent_item_code",
"schedule_date",
"column_break_3",
"qty",
"bom_no",
"bom_level",
"type_of_manufacturing",
"section_break_4rxf",
"required_qty",
"column_break_xfhm",
"projected_qty",
"qty",
"subcontracting_section",
"supplier",
"work_order_details_section",
"wo_produced_qty",
"purchase_order",
"work_order_details_section",
"production_plan_item",
"wo_produced_qty",
"stock_reserved_qty",
"column_break_7",
"received_qty",
"indent",
"section_break_19",
"schedule_date",
"uom",
"stock_uom",
"column_break_22",
"description",
"section_break_4rxf",
"actual_qty",
"column_break_xfhm",
"projected_qty"
"column_break_22",
"description"
],
"fields": [
{
@@ -49,18 +52,18 @@
"depends_on": "eval:doc.type_of_manufacturing == \"In House\"",
"fieldname": "work_order_details_section",
"fieldtype": "Section Break",
"label": "Reference"
"label": "Manufacturing"
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
},
{
"columns": 1,
"columns": 2,
"fieldname": "qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Required Qty",
"label": "Qty to Order",
"read_only": 1
},
{
@@ -152,7 +155,6 @@
"columns": 2,
"fieldname": "fg_warehouse",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Target Warehouse",
"options": "Warehouse"
},
@@ -167,22 +169,24 @@
{
"fieldname": "supplier",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Supplier",
"mandatory_depends_on": "eval:doc.type_of_manufacturing == 'Subcontract'",
"mandatory_depends_on": "eval:doc.type_of_manufacturing == 'Subcontract' && doc.qty > 0",
"options": "Supplier"
},
{
"columns": 1,
"columns": 2,
"fieldname": "schedule_date",
"fieldtype": "Datetime",
"in_list_view": 1,
"label": "Schedule Date"
},
{
"fieldname": "section_break_4rxf",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"label": "Quantity"
},
{
"columns": 2,
"fieldname": "actual_qty",
"fieldtype": "Float",
"label": "Actual Qty",
@@ -194,8 +198,10 @@
"fieldtype": "Column Break"
},
{
"columns": 2,
"fieldname": "projected_qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Projected Qty",
"no_copy": 1,
"read_only": 1
@@ -205,20 +211,42 @@
"fieldtype": "Float",
"label": "Produced Qty",
"read_only": 1
},
{
"columns": 2,
"fieldname": "required_qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Required Qty"
},
{
"fieldname": "subcontracting_section",
"fieldtype": "Section Break",
"label": "Subcontracting"
},
{
"fieldname": "stock_reserved_qty",
"fieldtype": "Float",
"label": "Stock Reserved Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-01-01 17:50:32.273610",
"modified": "2025-05-01 14:28:35.979941",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Sub Assembly Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -32,7 +32,9 @@ class ProductionPlanSubAssemblyItem(Document):
purchase_order: DF.Link | None
qty: DF.Float
received_qty: DF.Float
required_qty: DF.Float
schedule_date: DF.Datetime | None
stock_reserved_qty: DF.Float
stock_uom: DF.Link | None
supplier: DF.Link | None
type_of_manufacturing: DF.Literal["In House", "Subcontract", "Material Request"]

View File

@@ -43,11 +43,6 @@
"skip_transfer",
"from_wip_warehouse",
"update_consumed_material_cost_in_project",
"serial_no_and_batch_for_finished_good_section",
"has_serial_no",
"has_batch_no",
"column_break_18",
"batch_size",
"time",
"planned_start_date",
"planned_end_date",
@@ -63,6 +58,11 @@
"column_break_24",
"corrective_operation_cost",
"total_operating_cost",
"serial_no_and_batch_for_finished_good_section",
"has_serial_no",
"has_batch_no",
"column_break_18",
"batch_size",
"more_info",
"description",
"stock_uom",
@@ -505,7 +505,7 @@
"depends_on": "eval:!doc.__islocal",
"fieldname": "serial_no_and_batch_for_finished_good_section",
"fieldtype": "Section Break",
"label": "Serial No and Batch for Finished Good"
"label": "Finished Good Serial / Batch"
},
{
"fieldname": "column_break_17",
@@ -594,12 +594,13 @@
"label": " Reserve Stock"
}
],
"grid_page_length": 50,
"icon": "fa fa-cogs",
"idx": 1,
"image_field": "image",
"is_submittable": 1,
"links": [],
"modified": "2024-09-23 16:56:00.483027",
"modified": "2025-04-25 11:46:38.739588",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order",
@@ -629,10 +630,11 @@
"role": "Stock User"
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "ASC",
"states": [],
"title_field": "production_item",
"track_changes": 1,
"track_seen": 1
}
}

View File

@@ -10,7 +10,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import Case
from frappe.query_builder.functions import Sum
from frappe.query_builder.functions import IfNull, Sum
from frappe.utils import (
cint,
date_diff,
@@ -1362,13 +1362,6 @@ class WorkOrder(Document):
)
)
if details.reserved_qty < details.transferred_qty:
frappe.throw(
_("Transferred Qty {0} cannot be greater than Reserved Qty {1} for item {2}").format(
details.transferred_qty, details.reserved_qty, item.item_code
)
)
@frappe.whitelist()
def make_bom(self):
data = frappe.db.sql(
@@ -1399,7 +1392,9 @@ class WorkOrder(Document):
items = frappe._dict()
stock_entry.reload()
if stock_entry.purpose == "Manufacture" and self.sales_order:
if stock_entry.purpose == "Manufacture" and (
self.sales_order or self.production_plan_sub_assembly_item
):
items = self.get_finished_goods_for_reservation(stock_entry)
elif stock_entry.purpose == "Material Transfer for Manufacture":
items = self.get_list_of_materials_for_reservation(stock_entry)
@@ -1412,7 +1407,7 @@ class WorkOrder(Document):
def get_list_of_materials_for_reservation(self, stock_entry):
items = frappe._dict()
vocher_detail_no = {d.item_code: d.name for d in self.required_items}
voucher_detail_no = {d.item_code: d.name for d in self.required_items}
for row in stock_entry.items:
if row.item_code not in items:
@@ -1420,7 +1415,7 @@ class WorkOrder(Document):
{
"voucher_no": self.name,
"voucher_type": self.doctype,
"voucher_detail_no": vocher_detail_no.get(row.item_code),
"voucher_detail_no": voucher_detail_no.get(row.item_code),
"item_code": row.item_code,
"warehouse": row.t_warehouse,
"stock_qty": row.transfer_qty,
@@ -1440,41 +1435,97 @@ class WorkOrder(Document):
def get_finished_goods_for_reservation(self, stock_entry):
items = frappe._dict()
so_details = self.get_so_details()
if not so_details:
return items
if self.production_plan_sub_assembly_item:
# Reserve the sub-assembly item for the final product for the work order.
item_details = self.get_wo_details()
else:
# Reserve the final product for the sales order.
item_details = self.get_so_details()
qty = so_details.stock_qty - (so_details.stock_reserved_qty + so_details.delivered_qty)
if not qty:
return items
for row in stock_entry.items:
if not row.t_warehouse or not row.is_finished_item:
for item in item_details:
qty_to_reserve = flt(item.stock_qty) - flt(item.stock_reserved_qty + item.delivered_qty)
if qty_to_reserve <= 0:
continue
if qty > row.transfer_qty:
qty = row.transfer_qty
warehouse = item.warehouse
if (
item.get("parenttype") == "Work Order"
and item.get("skip_transfer")
and item.get("from_wip_warehouse")
):
warehouse = item.wip_warehouse
if row.item_code not in items:
items[row.item_code] = frappe._dict(
{
"voucher_no": self.sales_order,
"voucher_type": "Sales Order",
"voucher_detail_no": so_details.name,
"item_code": row.item_code,
"warehouse": row.t_warehouse,
"stock_qty": qty,
"from_voucher_no": stock_entry.name,
"from_voucher_type": stock_entry.doctype,
"from_voucher_detail_no": row.name,
"serial_and_batch_bundles": [row.serial_and_batch_bundle],
}
)
else:
items[row.item_code]["stock_qty"] += qty
for row in stock_entry.items:
if (
not row.t_warehouse
or not row.is_finished_item
or row.t_warehouse != warehouse
or row.item_code != item.item_code
):
continue
reserved_qty = qty_to_reserve
if qty_to_reserve > row.transfer_qty:
reserved_qty = row.transfer_qty
qty_to_reserve -= row.transfer_qty
else:
qty_to_reserve = 0
if row.item_code not in items:
items[row.item_code] = frappe._dict(
{
"voucher_no": item.voucher_no,
"voucher_type": item.voucher_type,
"voucher_detail_no": item.name,
"item_code": row.item_code,
"warehouse": row.t_warehouse,
"stock_qty": reserved_qty,
"from_voucher_no": stock_entry.name,
"from_voucher_type": stock_entry.doctype,
"from_voucher_detail_no": row.name,
"serial_and_batch_bundles": [row.serial_and_batch_bundle],
}
)
else:
items[row.item_code]["stock_qty"] += reserved_qty
return items
def get_wo_details(self):
doctype = frappe.qb.DocType("Work Order")
child_doctype = frappe.qb.DocType("Work Order Item")
query = (
frappe.qb.from_(doctype)
.inner_join(child_doctype)
.on(doctype.name == child_doctype.parent)
.select(
child_doctype.name,
child_doctype.required_qty.as_("stock_qty"),
child_doctype.transferred_qty.as_("delivered_qty"),
child_doctype.stock_reserved_qty,
child_doctype.source_warehouse.as_("warehouse"),
doctype.wip_warehouse,
doctype.skip_transfer,
doctype.from_wip_warehouse,
child_doctype.parenttype,
child_doctype.item_code,
child_doctype.parent.as_("voucher_no"),
child_doctype.parenttype.as_("voucher_type"),
)
.where(
(child_doctype.item_code == self.production_item)
& (doctype.docstatus == 1)
& (doctype.production_plan == self.production_plan)
& (
IfNull(doctype.production_plan_sub_assembly_item, "")
!= self.production_plan_sub_assembly_item
)
)
)
return query.run(as_dict=1)
def get_so_details(self):
return frappe.db.get_value(
"Sales Order Item",
@@ -1483,7 +1534,15 @@ class WorkOrder(Document):
"item_code": self.production_item,
"docstatus": 1,
},
["name", "stock_qty", "stock_reserved_qty", "delivered_qty"],
[
"name",
"stock_qty",
"stock_reserved_qty",
"warehouse",
"parent as voucher_no",
"parenttype as voucher_type",
"delivered_qty",
],
as_dict=1,
)
@@ -1524,9 +1583,49 @@ class WorkOrder(Document):
if sre_list:
cancel_stock_reservation_entries(self, sre_list)
def remove_additional_items(self, stock_entry):
for row in stock_entry.items:
for item in self.required_items:
if row.item_code == item.item_code and row.name == item.voucher_detail_reference:
item.delete()
def add_additional_items(self, stock_entry):
if stock_entry.purpose != "Material Transfer for Manufacture":
return
required_items = [d.item_code for d in self.required_items]
additional_items = frappe._dict()
for row in stock_entry.items:
if row.item_code not in required_items:
additional_items.setdefault(row.item_code, []).append(row)
for item_code, rows in additional_items.items():
for row in rows:
child_row = self.append(
"required_items",
{
"item_code": item_code,
"source_warehouse": row.s_warehouse,
"item_name": row.item_name,
"required_qty": row.transfer_qty,
"stock_uom": row.stock_uom,
"rate": row.basic_rate,
"amount": row.amount,
"description": row.description,
"docstatus": 1,
"is_additional_item": 1,
"voucher_detail_reference": row.name,
},
)
child_row.insert()
stock_entry.reload()
@frappe.whitelist()
def make_stock_reservation_entries(doc, items=None, notify=False):
def make_stock_reservation_entries(doc, items=None, table_name=None, notify=False):
if isinstance(doc, str):
doc = parse_json(doc)
doc = frappe.get_doc("Work Order", doc.get("name"))
@@ -1536,7 +1635,13 @@ def make_stock_reservation_entries(doc, items=None, notify=False):
sre = StockReservation(doc, items=items, notify=notify)
if doc.docstatus == 1:
sre.make_stock_reservation_entries()
if doc.production_plan:
sre.transfer_reservation_entries_to(
doc.production_plan, from_doctype="Production Plan", to_doctype="Work Order"
)
else:
sre.make_stock_reservation_entries()
frappe.msgprint(_("Stock Reservation Entries Created"), alert=True)
elif doc.docstatus == 2:
sre.cancel_stock_reservation_entries()

View File

@@ -27,7 +27,9 @@
"available_qty_at_source_warehouse",
"available_qty_at_wip_warehouse",
"column_break_jash",
"stock_reserved_qty"
"stock_reserved_qty",
"is_additional_item",
"voucher_detail_reference"
],
"fields": [
{
@@ -172,19 +174,37 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"default": "0",
"fieldname": "is_additional_item",
"fieldtype": "Check",
"label": "Is Additional Item",
"no_copy": 1,
"read_only": 1
},
{
"depends_on": "is_additional_item",
"fieldname": "voucher_detail_reference",
"fieldtype": "Data",
"label": "Voucher Detail Reference",
"no_copy": 1,
"read_only": 1
}
],
"grid_page_length": 50,
"istable": 1,
"links": [],
"modified": "2024-11-20 15:48:16.823384",
"modified": "2025-05-12 17:36:00.115181",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -22,6 +22,7 @@ class WorkOrderItem(Document):
consumed_qty: DF.Float
description: DF.Text | None
include_item_in_manufacturing: DF.Check
is_additional_item: DF.Check
item_code: DF.Link | None
item_name: DF.Data | None
operation: DF.Link | None
@@ -33,9 +34,10 @@ class WorkOrderItem(Document):
required_qty: DF.Float
returned_qty: DF.Float
source_warehouse: DF.Link | None
stock_uom: DF.Link | None
stock_reserved_qty: DF.Float
stock_uom: DF.Link | None
transferred_qty: DF.Float
voucher_detail_reference: DF.Data | None
# end: auto-generated types
pass

View File

@@ -23,6 +23,7 @@ def get_columns():
"""return columns"""
columns = [
_("Item") + ":Link/Item:150",
_("Item Name") + "::240",
_("Description") + "::300",
_("BOM Qty") + ":Float:160",
_("BOM UoM") + "::160",
@@ -73,6 +74,7 @@ def get_bom_stock(filters):
.on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS))
.select(
BOM_ITEM.item_code,
BOM_ITEM.item_name,
BOM_ITEM.description,
BOM_ITEM.stock_qty,
BOM_ITEM.stock_uom,

View File

@@ -94,6 +94,7 @@ def get_expected_data(bom, warehouse, qty_to_produce, show_exploded_view=False):
expected_data.append(
[
item.item_code,
item.item_name,
item.description,
item.stock_qty,
item.stock_uom,

View File

@@ -29,6 +29,9 @@ def get_data(report_filters):
if d.consumed_qty and d.consumed_qty > d.required_qty:
d.extra_consumed_qty = d.consumed_qty - d.required_qty
if d.is_additional_item:
d.extra_consumed_qty = d.consumed_qty
if d.extra_consumed_qty or not report_filters.show_extra_consumed_materials:
wo_items.setdefault((d.name, d.production_item), []).append(d)
@@ -81,6 +84,7 @@ def get_fields():
"`tabWork Order Item`.`required_qty`",
"`tabWork Order Item`.`transferred_qty`",
"`tabWork Order Item`.`consumed_qty`",
"`tabWork Order Item`.`is_additional_item`",
"`tabWork Order`.`status`",
"`tabWork Order`.`name`",
"`tabWork Order`.`production_item`",

View File

@@ -412,3 +412,4 @@ erpnext.patches.v15_0.rename_group_by_to_categorize_by
execute:frappe.db.set_single_value("Accounts Settings", "receivable_payable_fetch_method", "Buffered Cursor")
erpnext.patches.v14_0.set_update_price_list_based_on
erpnext.patches.v15_0.update_journal_entry_type
erpnext.patches.v15_0.set_grand_total_to_default_mop

View File

@@ -0,0 +1,9 @@
import frappe
def execute():
POSProfile = frappe.qb.DocType("POS Profile")
frappe.qb.update(POSProfile).set(POSProfile.set_grand_total_to_default_mop, 1).where(
POSProfile.disable_grand_total_to_default_mop == 0
).run()

View File

@@ -522,7 +522,7 @@ def get_timesheets_list(doctype, txt, filters, limit_start, limit_page_length=20
table.name,
child_table.activity_type,
table.status,
table.total_billable_hours,
child_table.billing_hours,
(table.sales_invoice | child_table.sales_invoice).as_("sales_invoice"),
child_table.project,
)

View File

@@ -64,7 +64,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
&& this.frm.doc.is_pos
&& this.frm.doc.is_return
) {
this.set_total_amount_to_default_mop();
await this.set_total_amount_to_default_mop();
this.calculate_paid_amount();
}
@@ -911,23 +911,25 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
it should set the return to that mode of payment only.
*/
let return_against_mop = await frappe.call({
method: 'erpnext.controllers.sales_and_purchase_return.get_payment_data',
args: {
invoice: this.frm.doc.return_against
}
});
if (return_against_mop.message.length === 1) {
this.frm.doc.payments.forEach(payment => {
if (payment.mode_of_payment == return_against_mop.message[0].mode_of_payment) {
payment.amount = total_amount_to_pay;
} else {
payment.amount = 0;
if(this.frm.doc.return_against){
let {message : return_against_mop } = await frappe.call({
method: 'erpnext.controllers.sales_and_purchase_return.get_payment_data',
args: {
invoice: this.frm.doc.return_against
}
});
this.frm.refresh_fields();
return;
if (return_against_mop.length === 1) {
this.frm.doc.payments.forEach(payment => {
if (payment.mode_of_payment == return_against_mop[0].mode_of_payment) {
payment.amount = total_amount_to_pay;
} else {
payment.amount = 0;
}
});
this.frm.refresh_fields();
return;
}
}
this.frm.doc.payments.find(payment => {
@@ -945,9 +947,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
var me = this;
var payment_status = true;
if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)) {
let r = await frappe.db.get_value("POS Profile", this.frm.doc.pos_profile, "disable_grand_total_to_default_mop");
let r = await frappe.db.get_value("POS Profile", this.frm.doc.pos_profile, "set_grand_total_to_default_mop");
if (r.message.disable_grand_total_to_default_mop) {
if (!r.message.set_grand_total_to_default_mop) {
return;
}

View File

@@ -847,8 +847,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
frappe.model.set_value(item.doctype, item.name, "stock_qty", valid_serial_nos.length);
}
validate() {
this.calculate_taxes_and_totals(false);
async validate() {
await this.calculate_taxes_and_totals(false);
}
update_stock() {

View File

@@ -47,7 +47,7 @@ frappe.ui.form.on("Event", {
frm.add_custom_button(
__("Add Sales Partners"),
function () {
new frappe.desk.eventParticipants(frm, "Sales Partners");
new frappe.desk.eventParticipants(frm, "Sales Partner");
},
__("Add Participants")
);

View File

@@ -14,7 +14,7 @@ $.extend(erpnext.stock_reservation, {
fields: erpnext.stock_reservation.get_dialog_fields(frm, parms),
primary_action_label: __("Reserve Stock"),
primary_action: () => {
erpnext.stock_reservation.reserve_stock(frm, parms);
erpnext.stock_reservation.reserve_stock(frm, table_name, parms);
},
});
@@ -32,9 +32,20 @@ $.extend(erpnext.stock_reservation, {
"Work Order": "required_qty",
}[frm.doc.doctype];
if (frm.doc.doctype === "Production Plan") {
if (table_name === "sub_assembly_items") {
params["qty_field"] = "qty";
params["item_code_field"] = "production_item";
params["warehouse_field"] = "fg_warehouse";
} else {
params["qty_field"] = "quantity";
}
}
params["dispatch_qty_field"] = {
"Sales Order": "delivered_qty",
"Work Order": "transferred_qty",
"Production Plan": "delivered_qty",
}[frm.doc.doctype];
params["method"] = {
@@ -140,6 +151,9 @@ $.extend(erpnext.stock_reservation, {
dispatch_qty_field = "consumed_qty";
}
let item_code_field = parms.item_code_field || "item_code";
let warehouse_field = parms.warehouse_field || "warehouse";
frm.doc[parms.table_name].forEach((item) => {
if (frm.doc.reserve_stock) {
let unreserved_qty =
@@ -152,8 +166,8 @@ $.extend(erpnext.stock_reservation, {
if (unreserved_qty > 0) {
let args = {
__checked: 1,
item_code: item.item_code,
warehouse: item.warehouse || item.source_warehouse,
item_code: item[item_code_field] || item.item_code,
warehouse: item[warehouse_field] || item.warehouse || item.source_warehouse,
};
args[field] = item.name;
@@ -167,7 +181,7 @@ $.extend(erpnext.stock_reservation, {
dialog.show();
},
reserve_stock(frm, parms) {
reserve_stock(frm, table_name, parms) {
let dialog = erpnext.stock_reservation.dialog;
var data = { items: dialog.fields_dict.items.grid.get_selected_children() };
@@ -177,6 +191,7 @@ $.extend(erpnext.stock_reservation, {
args: {
doc: frm.doc,
items: data.items,
table_name: table_name,
notify: true,
},
freeze: true,

View File

@@ -432,8 +432,9 @@ $.extend(erpnext.utils, {
if (!frappe.boot.setup_complete) {
return;
}
const today = frappe.datetime.get_today();
if (!date) {
date = frappe.datetime.get_today();
date = today;
}
let fiscal_year = "";
@@ -444,7 +445,7 @@ $.extend(erpnext.utils, {
) {
if (with_dates) fiscal_year = frappe.boot.current_fiscal_year;
else fiscal_year = frappe.boot.current_fiscal_year[0];
} else {
} else if (today != date) {
frappe.call({
method: "erpnext.accounts.utils.get_fiscal_year",
type: "GET", // make it cacheable

View File

@@ -5,9 +5,9 @@
padding: 1%;
section {
min-height: 45rem;
height: calc(100vh - 200px);
max-height: calc(100vh - 200px);
min-height: 30rem;
height: calc(100vh - 125px);
max-height: calc(100vh - 125px);
}
.frappe-control {
@@ -375,6 +375,7 @@
flex-direction: column;
flex: 1 1 0%;
overflow-y: scroll;
min-height: 50px;
> .cart-item-wrapper {
@extend .pointer-no-select;
@@ -775,6 +776,7 @@
.submit-order-btn {
@extend .primary-action;
margin-top: 0%;
background-color: var(--btn-primary);
color: var(--neutral);
}
@@ -785,117 +787,136 @@
margin-bottom: var(--margin-md);
}
> .payment-modes {
> .payment-split-container {
display: flex;
padding-bottom: var(--padding-sm);
margin-bottom: var(--margin-sm);
overflow-x: scroll;
overflow-y: hidden;
flex-shrink: 0;
> .payment-mode-wrapper {
min-width: 40%;
padding: var(--padding-xs);
> .payment-container-left {
width: 50%;
margin-bottom: var(--margin-md);
> .mode-of-payment {
@extend .pos-card;
@extend .pointer-no-select;
padding: var(--padding-md) var(--padding-lg);
.payment-modes {
display: flex;
flex-direction: column;
padding-right: var(--padding-sm);
margin-right: var(--margin-sm);
min-height: 15rem;
overflow-y: scroll;
height: calc(100vh - 350px);
> .pay-amount {
display: inline;
float: right;
font-weight: 700;
}
> .payment-mode-wrapper {
min-width: 40%;
padding: var(--padding-xs);
> .mode-of-payment-control {
display: none;
align-items: center;
margin-top: var(--margin-sm);
margin-bottom: var(--margin-xs);
}
> .loyalty-amount-name {
display: none;
float: right;
font-weight: 700;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
> .cash-shortcuts {
display: none;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--margin-sm);
font-size: var(--text-sm);
text-align: center;
> .shortcut {
> .mode-of-payment {
@extend .pos-card;
@extend .pointer-no-select;
border-radius: var(--border-radius-sm);
background-color: var(--control-bg);
font-weight: 500;
padding: var(--padding-xs) var(--padding-sm);
transition: all 0.15s ease-in-out;
padding: var(--padding-md) var(--padding-lg);
&:hover {
background-color: var(--control-bg);
> .pay-amount {
display: inline;
float: right;
font-weight: 700;
}
> .mode-of-payment-control {
display: none;
align-items: center;
margin-top: var(--margin-sm);
margin-bottom: var(--margin-xs);
}
> .loyalty-amount-name {
display: none;
float: right;
font-weight: 700;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
> .cash-shortcuts {
display: none;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--margin-sm);
font-size: var(--text-sm);
text-align: center;
> .shortcut {
@extend .pointer-no-select;
border-radius: var(--border-radius-sm);
background-color: var(--control-bg);
font-weight: 500;
padding: var(--padding-xs) var(--padding-sm);
transition: all 0.15s ease-in-out;
&:hover {
background-color: var(--control-bg);
}
}
}
}
> .loyalty-card {
display: flex;
flex-direction: column;
}
}
}
> .loyalty-card {
display: flex;
flex-direction: column;
}
}
}
> .fields-numpad-container {
display: flex;
flex: 1;
height: 100%;
position: relative;
justify-content: flex-end;
> .fields-section {
flex: 1;
> .payment-container-right {
display: flex;
flex-direction: column;
width: 50%;
height: 100%;
padding-bottom: var(--margin-md);
.invoice-fields {
overflow-y: scroll;
.fields-numpad-container {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
padding-right: var(--padding-sm);
}
}
position: relative;
justify-content: flex-end;
> .number-pad {
flex: 1;
display: flex;
justify-content: flex-end;
align-items: flex-end;
max-width: 50%;
.numpad-container {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--margin-md);
margin-bottom: var(--margin-md);
> .numpad-btn {
@extend .pointer-no-select;
border-radius: var(--border-radius-md);
> .fields-section {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: var(--padding-md);
box-shadow: var(--shadow-sm);
flex-direction: column;
padding-left: var(--margin-md);
.invoice-fields {
height: 100%;
margin-left: auto;
padding: var(--padding-sm);
}
}
.number-pad {
position: absolute;
z-index: 4;
right: 0px;
flex: 1;
display: flex;
align-items: flex-end;
.numpad-container {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--margin-md);
margin-bottom: var(--margin-md);
background-color: var(--fg-color);
border-radius: var(--border-radius-md);
padding: var(--padding-sm);
> .numpad-btn {
@extend .pointer-no-select;
border-radius: var(--border-radius-md);
display: flex;
align-items: center;
justify-content: center;
padding: var(--padding-md);
box-shadow: var(--shadow-base);
}
}
}
}
}

View File

@@ -147,10 +147,8 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
bin_join_selection, bin_join_condition = "", ""
if hide_unavailable_items:
bin_join_selection = ", `tabBin` bin"
bin_join_condition = (
"AND bin.warehouse = %(warehouse)s AND bin.item_code = item.name AND bin.actual_qty > 0"
)
bin_join_selection = "LEFT JOIN `tabBin` bin ON bin.item_code = item.name"
bin_join_condition = "AND item.is_stock_item = 0 OR (item.is_stock_item = 1 AND bin.warehouse = %(warehouse)s AND bin.actual_qty > 0)"
items_data = frappe.db.sql(
"""

View File

@@ -156,6 +156,28 @@ erpnext.PointOfSale.Controller = class {
},
});
this.fetch_invoice_fields();
this.setup_listener_for_pos_closing();
}
fetch_invoice_fields() {
const me = this;
frappe.db.get_doc("POS Settings", undefined).then((doc) => {
me.settings.invoice_fields = doc.invoice_fields.map((field) => {
return {
fieldname: field.fieldname,
label: field.label,
fieldtype: field.fieldtype,
reqd: field.reqd,
options: field.options,
default_value: field.default_value,
read_only: field.read_only,
};
});
});
}
setup_listener_for_pos_closing() {
frappe.realtime.on(`poe_${this.pos_opening}_closed`, (data) => {
const route = frappe.get_route_str();
if (data && route == "point-of-sale") {
@@ -426,6 +448,7 @@ erpnext.PointOfSale.Controller = class {
init_payments() {
this.payment = new erpnext.PointOfSale.Payment({
wrapper: this.$components_wrapper,
settings: this.settings,
events: {
get_frm: () => this.frm || {},

View File

@@ -171,6 +171,9 @@ erpnext.PointOfSale.ItemCart = class {
me.toggle_item_highlight(this);
const scrollTop = $cart_item.offset().top - me.$cart_items_wrapper.offset().top;
me.$cart_items_wrapper.animate({ scrollTop });
const payment_section_hidden = !me.$totals_section.find(".edit-cart-btn").is(":visible");
if (!payment_section_hidden) {
// payment section is visible

View File

@@ -1,8 +1,10 @@
/* eslint-disable no-unused-vars */
erpnext.PointOfSale.Payment = class {
constructor({ events, wrapper }) {
constructor({ events, wrapper, settings }) {
this.wrapper = wrapper;
this.events = events;
this.set_gt_to_default_mop = settings.set_grand_total_to_default_mop;
this.invoice_fields = settings.invoice_fields;
this.init_component();
}
@@ -17,14 +19,23 @@ erpnext.PointOfSale.Payment = class {
prepare_dom() {
this.wrapper.append(
`<section class="payment-container">
<div class="section-label payment-section">${__("Payment Method")}</div>
<div class="payment-modes"></div>
<div class="fields-numpad-container">
<div class="fields-section">
<div class="section-label">${__("Additional Information")}</div>
<div class="invoice-fields"></div>
<div class="payment-split-container">
<div class="payment-container-left">
<div class="section-label payment-section">${__("Payment Method")}</div>
<div class="payment-modes"></div>
</div>
<div class="payment-container-right">
<div class="fields-numpad-container">
<div class="fields-section">
<div class="invoice-fields">
<button class="btn btn-default btn-sm btn-shadow addl-fields hidden">${__(
"Update Additional Information"
)}</button>
</div>
</div>
<div class="number-pad"></div>
</div>
</div>
<div class="number-pad"></div>
</div>
<div class="totals-section">
<div class="totals"></div>
@@ -40,48 +51,61 @@ erpnext.PointOfSale.Payment = class {
this.$invoice_fields_section = this.$component.find(".fields-section");
}
make_invoice_fields_control() {
this.reqd_invoice_fields = [];
frappe.db.get_doc("POS Settings", undefined).then((doc) => {
const fields = doc.invoice_fields;
if (!fields.length) return;
make_invoice_field_dialog() {
const me = this;
if (!me.invoice_fields.length) return;
me.addl_dlg = new frappe.ui.Dialog({
title: __("Additional Information"),
fields: me.invoice_fields,
size: "small",
primary_action_label: __("Save"),
primary_action(values) {
me.set_values_to_frm(values);
this.hide();
},
});
me.add_btn_field_click_listener();
me.set_value_on_dialog_fields();
me.make_addl_info_dialog_btn_visible();
}
this.$invoice_fields = this.$invoice_fields_section.find(".invoice-fields");
this.$invoice_fields.html("");
const frm = this.events.get_frm();
set_values_to_frm(values) {
const frm = this.events.get_frm();
for (const value in values) {
frm.set_value(value, values[value]);
}
frappe.show_alert({
message: __("Additional Information updated successfully."),
indicator: "green",
});
}
fields.forEach((df) => {
this.$invoice_fields.append(
`<div class="invoice_detail_field ${df.fieldname}-field" data-fieldname="${df.fieldname}"></div>`
);
let df_events = {
onchange: function () {
frm.set_value(this.df.fieldname, this.get_value());
},
};
if (df.fieldtype == "Button") {
df_events = {
click: function () {
if (frm.script_manager.has_handlers(df.fieldname, frm.doc.doctype)) {
frm.script_manager.trigger(df.fieldname, frm.doc.doctype, frm.doc.docname);
}
},
};
}
if (df.reqd && (df.fieldtype !== "Button" || !df.read_only)) {
this.reqd_invoice_fields.push({ fieldname: df.fieldname, label: df.label });
}
this[`${df.fieldname}_field`] = frappe.ui.form.make_control({
df: {
...df,
...df_events,
},
parent: this.$invoice_fields.find(`.${df.fieldname}-field`),
render_input: true,
add_btn_field_click_listener() {
const frm = this.events.get_frm();
this.addl_dlg.fields.forEach((df) => {
if (df.fieldtype === "Button") {
this.addl_dlg.fields_dict[df.fieldname].$input.on("click", function () {
if (frm.script_manager.has_handlers(df.fieldname, frm.doc.doctype)) {
frm.script_manager.trigger(df.fieldname, frm.doc.doctype, frm.doc.docname);
}
});
this[`${df.fieldname}_field`].set_value(frm.doc[df.fieldname]);
});
}
});
}
set_value_on_dialog_fields() {
const doc = this.events.get_frm().doc;
this.addl_dlg.fields.forEach((df) => {
if (doc[df.fieldname] || df.default_value) {
this.addl_dlg.set_value(df.fieldname, doc[df.fieldname] || df.default_value);
}
});
}
make_addl_info_dialog_btn_visible() {
this.$invoice_fields_section.find(".addl-fields").removeClass("hidden");
this.$invoice_fields_section.find(".addl-fields").on("click", () => {
this.addl_dlg.show();
});
}
@@ -164,6 +188,16 @@ erpnext.PointOfSale.Payment = class {
}
});
frappe.ui.form.on("POS Invoice", "contact_mobile", (frm) => {
const contact = frm.doc.contact_mobile;
const request_button = $(this.request_for_payment_field?.$input[0]);
if (contact) {
request_button.removeClass("btn-default").addClass("btn-primary");
} else {
request_button.removeClass("btn-primary").addClass("btn-default");
}
});
frappe.ui.form.on("POS Invoice", "coupon_code", (frm) => {
this.bind_coupon_code_event(frm);
});
@@ -355,9 +389,9 @@ erpnext.PointOfSale.Payment = class {
render_payment_section() {
this.render_payment_mode_dom();
this.make_invoice_fields_control();
this.make_invoice_field_dialog();
this.update_totals_section();
this.unset_grand_total_to_default_mop();
this.set_grand_total_to_default_mop();
}
after_render() {
@@ -610,7 +644,7 @@ erpnext.PointOfSale.Payment = class {
const remaining = grand_total - doc.paid_amount;
const change = doc.change_amount || remaining <= 0 ? -1 * remaining : undefined;
const currency = doc.currency;
const label = __("Change Amount");
const label = doc.paid_amount > grand_total ? __("Change Amount") : __("Remaining Amount");
this.$totals.html(
`<div class="col">
@@ -642,32 +676,28 @@ erpnext.PointOfSale.Payment = class {
.toLowerCase();
}
async unset_grand_total_to_default_mop() {
const doc = this.events.get_frm().doc;
let r = await frappe.db.get_value(
"POS Profile",
doc.pos_profile,
"disable_grand_total_to_default_mop"
);
if (!r.message.disable_grand_total_to_default_mop) {
set_grand_total_to_default_mop() {
if (this.set_gt_to_default_mop) {
this.focus_on_default_mop();
}
}
validate_reqd_invoice_fields() {
const doc = this.events.get_frm().doc;
let validation_flag = true;
for (let field of this.reqd_invoice_fields) {
if (!doc[field.fieldname]) {
validation_flag = false;
for (const df of this.addl_dlg.fields) {
if (df.reqd && !doc[df.fieldname]) {
frappe.show_alert({
message: __("{0} is a mandatory field.", [field.label]),
indicator: "orange",
message: __(
"Invoice cannot be submitted without filling the mandatory Additional Information fields."
),
indicator: "red",
});
frappe.utils.play_sound("error");
this.addl_dlg.show();
this.addl_dlg.fields_dict[df.fieldname].$input.focus();
return false;
}
}
return validation_flag;
return true;
}
};

View File

@@ -3,6 +3,7 @@
import frappe
from frappe.defaults import get_user_default
from frappe.utils import cint
import erpnext.accounts.utils
@@ -57,7 +58,9 @@ def boot_session(bootinfo):
party_account_types = frappe.db.sql(""" select name, ifnull(account_type, '') from `tabParty Type`""")
bootinfo.party_account_types = frappe._dict(party_account_types)
fiscal_year = erpnext.accounts.utils.get_fiscal_years(frappe.utils.nowdate(), raise_on_missing=False)
fiscal_year = erpnext.accounts.utils.get_fiscal_years(
frappe.utils.nowdate(), company=get_user_default("company"), raise_on_missing=False
)
if fiscal_year:
bootinfo.current_fiscal_year = fiscal_year[0]

View File

@@ -17,6 +17,7 @@ from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.accounts_controller import merge_taxes
from erpnext.controllers.buying_controller import BuyingController
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import StockReservation
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
@@ -383,7 +384,7 @@ class PurchaseReceipt(BuyingController):
self.make_gl_entries()
self.repost_future_sle_and_gle()
self.set_consumed_qty_in_subcontract_order()
self.reserve_stock_for_sales_order()
self.reserve_stock()
self.update_received_qty_if_from_pp()
def update_received_qty_if_from_pp(self):
@@ -913,6 +914,10 @@ class PurchaseReceipt(BuyingController):
pr_doc = self if (pr == self.name) else frappe.get_doc("Purchase Receipt", pr)
update_billing_percentage(pr_doc, update_modified=update_modified)
def reserve_stock(self):
self.reserve_stock_for_sales_order()
self.reserve_stock_for_production_plan()
def reserve_stock_for_sales_order(self):
if (
self.is_return
@@ -953,6 +958,66 @@ class PurchaseReceipt(BuyingController):
notify=True,
)
def reserve_stock_for_production_plan(self):
if self.is_return or not frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"):
return
production_plan_references = self.get_production_plan_references()
production_plan_items = []
docnames = []
for row in self.items:
if row.material_request_item and row.material_request_item in production_plan_references:
_ref = production_plan_references[row.material_request_item]
docnames.append(_ref.production_plan)
row.update(
{
"voucher_type": "Production Plan",
"voucher_no": _ref.production_plan,
"voucher_detail_no": _ref.material_request_plan_item,
"from_voucher_no": self.name,
"from_voucher_detail_no": row.name,
"from_voucher_type": self.doctype,
}
)
production_plan_items.append(row)
if not production_plan_items:
return
sre = StockReservation(doc=self, items=production_plan_items)
sre.make_stock_reservation_entries()
if docnames:
sre.transfer_reservation_entries_to(
docnames, from_doctype="Production Plan", to_doctype="Work Order"
)
def get_production_plan_references(self):
production_plan_references = frappe._dict()
material_request_items = []
for row in self.items:
if row.material_request_item:
material_request_items.append(row.material_request_item)
if not material_request_items:
return frappe._dict()
items = frappe.get_all(
"Material Request Item",
fields=["material_request_plan_item", "production_plan", "name"],
filters={"name": ["in", material_request_items], "docstatus": 1},
)
for item in items:
if not item.production_plan:
continue
production_plan_references.setdefault(item.name, item)
return production_plan_references
def enable_recalculate_rate_in_sles(self):
rejected_warehouses = frappe.get_all(
"Purchase Receipt Item", filters={"parent": self.name}, pluck="rejected_warehouse"
@@ -1105,6 +1170,7 @@ def get_billed_amount_against_po(po_items):
def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate=False):
# Update Billing % based on pending accepted qty
buying_settings = frappe.get_single("Buying Settings")
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
total_amount, total_billed_amount = 0, 0
item_wise_returned_qty = get_item_wise_returned_qty(pr_doc)
@@ -1143,6 +1209,14 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
adjusted_amt = flt(adjusted_amt * flt(pr_doc.conversion_rate), item.precision("amount"))
item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False)
elif item.billed_amt > item.amount:
per_over_billed = (flt(item.billed_amt / item.amount, 2) * 100) - 100
if per_over_billed > over_billing_allowance:
frappe.throw(
_("Over Billing Allowance exceeded for Purchase Receipt Item {0} ({1}) by {2}%").format(
item.name, frappe.bold(item.item_code), per_over_billed - over_billing_allowance
)
)
percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6)
pr_doc.db_set("per_billed", percent_billed)

View File

@@ -1635,6 +1635,11 @@ class StockEntry(StockController):
_validate_work_order(pro_doc)
if self.fg_completed_qty:
if self.docstatus == 1:
pro_doc.add_additional_items(self)
else:
pro_doc.remove_additional_items(self)
pro_doc.run_method("update_work_order_qty")
if self.purpose == "Manufacture":
pro_doc.run_method("update_planned_qty")
@@ -1646,7 +1651,11 @@ class StockEntry(StockController):
def make_stock_reserve_for_wip_and_fg(self):
if self.is_stock_reserve_for_work_order():
pro_doc = frappe.get_doc("Work Order", self.work_order)
if self.purpose == "Manufacture" and not pro_doc.sales_order:
if (
self.purpose == "Manufacture"
and not pro_doc.sales_order
and not pro_doc.production_plan_sub_assembly_item
):
return
pro_doc.set_reserved_qty_for_wip_and_fg(self)
@@ -1654,7 +1663,11 @@ class StockEntry(StockController):
def cancel_stock_reserve_for_wip_and_fg(self):
if self.is_stock_reserve_for_work_order():
pro_doc = frappe.get_doc("Work Order", self.work_order)
if self.purpose == "Manufacture" and not pro_doc.sales_order:
if (
self.purpose == "Manufacture"
and not pro_doc.sales_order
and not pro_doc.production_plan_sub_assembly_item
):
return
pro_doc.cancel_reserved_qty_for_wip_and_fg(self)

View File

@@ -84,7 +84,7 @@
"no_copy": 1,
"oldfieldname": "voucher_type",
"oldfieldtype": "Data",
"options": "\nSales Order\nWork Order",
"options": "\nSales Order\nWork Order\nProduction Plan",
"print_width": "150px",
"read_only": 1,
"width": "150px"
@@ -289,7 +289,7 @@
"fieldtype": "Select",
"label": "From Voucher Type",
"no_copy": 1,
"options": "\nPick List\nPurchase Receipt\nStock Entry\nWork Order",
"options": "\nPick List\nPurchase Receipt\nStock Entry\nWork Order\nProduction Plan",
"print_hide": 1,
"read_only": 1,
"report_hide": 1
@@ -339,12 +339,13 @@
"label": "Qty in WIP Warehouse"
}
],
"grid_page_length": 50,
"hide_toolbar": 1,
"in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-09-19 15:28:24.726283",
"modified": "2025-04-30 22:15:22.998138",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reservation Entry",
@@ -450,8 +451,9 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -30,7 +30,9 @@ class StockReservationEntry(Document):
delivered_qty: DF.Float
from_voucher_detail_no: DF.Data | None
from_voucher_no: DF.DynamicLink | None
from_voucher_type: DF.Literal["", "Pick List", "Purchase Receipt", "Stock Entry", "Work Order"]
from_voucher_type: DF.Literal[
"", "Pick List", "Purchase Receipt", "Stock Entry", "Work Order", "Production Plan"
]
has_batch_no: DF.Check
has_serial_no: DF.Check
item_code: DF.Link | None
@@ -46,7 +48,7 @@ class StockReservationEntry(Document):
voucher_detail_no: DF.Data | None
voucher_no: DF.DynamicLink | None
voucher_qty: DF.Float
voucher_type: DF.Literal["", "Sales Order", "Work Order"]
voucher_type: DF.Literal["", "Sales Order", "Work Order", "Production Plan"]
warehouse: DF.Link | None
# end: auto-generated types
@@ -335,6 +337,7 @@ class StockReservationEntry(Document):
item_doctype = {
"Sales Order": "Sales Order Item",
"Work Order": "Work Order Item",
"Production Plan": "Production Plan Sub Assembly Item",
}.get(self.voucher_type, None)
if item_doctype:
@@ -350,6 +353,11 @@ class StockReservationEntry(Document):
)
).run(as_list=True)[0][0] or 0
if self.voucher_type == "Production Plan" and frappe.db.exists(
"Material Request Plan Item", self.voucher_detail_no
):
item_doctype = "Material Request Plan Item"
frappe.db.set_value(
item_doctype,
self.voucher_detail_no,
@@ -968,13 +976,14 @@ def has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: st
class StockReservation:
def __init__(self, doc, items=None, notify=True):
def __init__(self, doc, items=None, kwargs=None, notify=True):
if isinstance(doc, str):
doc = parse_json(doc)
doc = frappe.get_doc("Work Order", doc.get("name"))
self.doc = doc
self.items = items
self.kwargs = kwargs
self.initialize_fields()
def initialize_fields(self) -> None:
@@ -989,6 +998,9 @@ class StockReservation:
self.warehouse_field = "source_warehouse"
if self.doc.skip_transfer and self.doc.from_wip_warehouse:
self.warehouse = self.doc.wip_warehouse
elif self.doc.doctype == "Production Plan" and self.kwargs:
for key, value in self.kwargs.items():
setattr(self, key, value)
def cancel_stock_reservation_entries(self, names=None) -> None:
"""Cancels Stock Reservation Entries for the Voucher."""
@@ -1037,11 +1049,16 @@ class StockReservation:
if isinstance(item, dict):
item = frappe._dict(item)
item_code = item.get("item_code") or item.get("production_item")
item_details = frappe.get_cached_value(
"Item", item.item_code, ["has_serial_no", "has_batch_no", "stock_uom"], as_dict=True
"Item", item_code, ["has_serial_no", "has_batch_no", "stock_uom"], as_dict=True
)
warehouse = self.warehouse or item.get(self.warehouse_field) or item.get("warehouse")
if self.doc.doctype == "Production Plan" and item.get("from_warehouse"):
warehouse = item.get("from_warehouse")
if (
not warehouse
and self.doc.doctype == "Work Order"
@@ -1052,10 +1069,12 @@ class StockReservation:
)
qty = item.get(self.qty_field) or item.get("stock_qty")
if not qty:
continue
self.available_qty_to_reserve = self.get_available_qty_to_reserve(item.item_code, warehouse)
self.available_qty_to_reserve = self.get_available_qty_to_reserve(item_code, warehouse)
if not self.available_qty_to_reserve:
self.throw_stock_not_exists_error(item, warehouse)
self.throw_stock_not_exists_error(item.idx, item_code, warehouse)
self.qty_to_be_reserved = (
qty if self.available_qty_to_reserve >= qty else self.available_qty_to_reserve
@@ -1064,7 +1083,7 @@ class StockReservation:
if not self.qty_to_be_reserved:
continue
sre.item_code = item.item_code
sre.item_code = item_code
sre.warehouse = warehouse
sre.has_serial_no = item_details.has_serial_no
sre.has_batch_no = item_details.has_batch_no
@@ -1107,10 +1126,10 @@ class StockReservation:
},
)
def throw_stock_not_exists_error(self, item, warehouse):
def throw_stock_not_exists_error(self, idx, item_code, warehouse):
frappe.msgprint(
_("Row #{0}: Stock not available to reserve for the Item {1} in Warehouse {2}.").format(
item.idx, frappe.bold(item.item_code), frappe.bold(warehouse)
idx, frappe.bold(item_code), frappe.bold(warehouse)
),
title=_("Stock Reservation"),
indicator="orange",
@@ -1143,6 +1162,163 @@ class StockReservation:
return available_qty
def transfer_reservation_entries_to(self, docnames, from_doctype, to_doctype):
delivery_qty_to_update = frappe._dict()
if isinstance(docnames, str):
docnames = [docnames]
items_to_reserve = self.get_items_to_reserve(docnames, from_doctype, to_doctype)
if not items_to_reserve:
return
reservation_entries = self.get_reserved_entries(from_doctype, docnames)
if not reservation_entries:
return
entries_to_reserve = []
for row in reservation_entries:
for entry in items_to_reserve:
if not (
row.item_code == entry.item_code and row.warehouse == entry.warehouse and entry.qty > 0
):
continue
available_qty = row.reserved_qty - row.delivered_qty
if available_qty <= 0:
continue
# transfer qty
if available_qty > entry.qty:
qty_to_reserve = entry.qty
row.delivered_qty += available_qty - entry.qty
delivery_qty_to_update.setdefault(row.name, row.delivered_qty)
else:
qty_to_reserve = available_qty
row.delivered_qty += qty_to_reserve
delivery_qty_to_update.setdefault(row.name, row.delivered_qty)
entries_to_reserve.append([entry, row, qty_to_reserve])
entry.qty -= qty_to_reserve
if delivery_qty_to_update:
self.update_delivered_qty(delivery_qty_to_update)
for entry, row, qty_to_reserve in entries_to_reserve:
self.make_stock_reservation_entry(entry, row, qty_to_reserve)
def update_delivered_qty(self, delivery_qty_to_update):
for name, delivered_qty in delivery_qty_to_update.items():
doctype = frappe.qb.DocType("Stock Reservation Entry")
query = (
frappe.qb.update(doctype)
.set(doctype.delivered_qty, delivered_qty)
.set(
doctype.status,
"Delivered" if doctype.reserved_qty == doctype.delivered_qty else "Reserved",
)
.where(doctype.name == name)
)
query.run()
def make_stock_reservation_entry(self, row, against_row, reserved_qty):
fields = [
"item_code",
"warehouse",
"voucher_type",
"voucher_no",
"voucher_detail_no",
"company",
"stock_uom",
]
sre = frappe.new_doc("Stock Reservation Entry")
for row_field in fields:
sre.set(row_field, row.get(row_field))
sre.available_qty = reserved_qty
sre.reserved_qty = reserved_qty
sre.voucher_qty = row.required_qty
sre.from_voucher_no = against_row.voucher_no
sre.from_voucher_detail_no = against_row.voucher_detail_no
sre.from_voucher_type = against_row.voucher_type
bundles = [against_row.name]
if row.serial_and_batch_bundles:
bundles = row.serial_and_batch_bundles
self.set_serial_batch(sre, bundles)
sre.save()
sre.submit()
def get_reserved_entries(self, doctype, docnames):
filters = {
"docstatus": 1,
"status": ("not in", ["Delivered", "Cancelled", "Draft"]),
"voucher_type": doctype,
"voucher_no": docnames,
}
if isinstance(docnames, list):
filters["voucher_no"] = ("in", docnames)
return frappe.get_all("Stock Reservation Entry", fields=["*"], filters=filters)
def get_items_to_reserve(self, docnames, from_doctype, to_doctype):
field = frappe.scrub(from_doctype)
doctype = frappe.qb.DocType(to_doctype)
child_doctype = frappe.qb.DocType(to_doctype + " Item")
query = (
frappe.qb.from_(doctype)
.inner_join(child_doctype)
.on(doctype.name == child_doctype.parent)
.select(
doctype.name.as_("voucher_no"),
child_doctype.name.as_("voucher_detail_no"),
child_doctype.item_code,
doctype.company,
child_doctype.stock_uom,
)
.where((doctype.docstatus == 1) & (doctype[field].isin(docnames)))
)
if to_doctype == "Work Order":
query = query.select(
child_doctype.source_warehouse,
doctype.wip_warehouse,
doctype.skip_transfer,
doctype.from_wip_warehouse,
child_doctype.required_qty,
(child_doctype.required_qty - child_doctype.transferred_qty).as_("qty"),
child_doctype.stock_reserved_qty,
)
query = query.where(
(doctype.qty > doctype.material_transferred_for_manufacturing)
& (doctype.status != "Completed")
)
data = query.run(as_dict=True)
items = []
for row in data:
if row.qty > row.stock_reserved_qty:
row.qty -= flt(row.stock_reserved_qty)
row.warehouse = row.source_warehouse
if row.skip_transfer and row.from_wip_warehouse:
row.warehouse = row.wip_warehouse
if to_doctype == "Work Order":
row.voucher_type = "Work Order"
items.append(row)
return items
def create_stock_reservation_entries_for_so_items(
sales_order: object,

View File

@@ -51,49 +51,11 @@ frappe.query_reports["Available Serial No"] = {
};
},
},
{
fieldname: "item_group",
label: __("Item Group"),
fieldtype: "Link",
options: "Item Group",
},
{
fieldname: "batch_no",
label: __("Batch No"),
fieldtype: "Link",
options: "Batch",
on_change() {
const batch_no = frappe.query_report.get_filter_value("batch_no");
if (batch_no) {
frappe.query_report.set_filter_value("segregate_serial_batch_bundle", 1);
} else {
frappe.query_report.set_filter_value("segregate_serial_batch_bundle", 0);
}
},
},
{
fieldname: "brand",
label: __("Brand"),
fieldtype: "Link",
options: "Brand",
},
{
fieldname: "voucher_no",
label: __("Voucher #"),
fieldtype: "Data",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "Link",
options: "Project",
},
{
fieldname: "include_uom",
label: __("Include UOM"),
fieldtype: "Link",
options: "UOM",
},
{
fieldname: "valuation_field_type",
label: __("Valuation Field Type"),

View File

@@ -3,108 +3,62 @@
import frappe
from frappe import _
from frappe.query_builder.functions import Sum
from frappe.utils import cint, flt
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, get_serial_nos_from_sle_list
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import get_stock_balance_for
from erpnext.stock.report.stock_ledger.stock_ledger import (
check_inventory_dimension_filters_applied,
get_item_details,
get_item_group_condition,
get_opening_balance,
get_opening_balance_from_batch,
get_stock_ledger_entries,
)
from erpnext.stock.utils import (
is_reposting_item_valuation_in_progress,
update_included_uom_in_report,
)
from erpnext.stock.utils import is_reposting_item_valuation_in_progress
def execute(filters=None):
is_reposting_item_valuation_in_progress()
include_uom = filters.get("include_uom")
columns = get_columns(filters)
items = get_items(filters)
sl_entries = get_stock_ledger_entries(filters, items)
item_details = get_item_details(items, sl_entries, include_uom)
item_details = get_item_details(items, sl_entries, False)
opening_row, actual_qty, stock_value = get_opening_balance_data(filters, columns, sl_entries)
opening_row = get_opening_balance_data(filters, columns, sl_entries)
precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
data, conversion_factors = process_stock_ledger_entries(
filters, sl_entries, item_details, opening_row, actual_qty, stock_value, precision
)
data = process_stock_ledger_entries(sl_entries, item_details, opening_row, precision)
update_included_uom_in_report(columns, data, include_uom, conversion_factors)
return columns, data
def get_opening_balance_data(filters, columns, sl_entries):
if filters.get("batch_no"):
opening_row = get_opening_balance_from_batch(filters, columns, sl_entries)
else:
opening_row = get_opening_balance(filters, columns, sl_entries)
actual_qty = opening_row.get("qty_after_transaction") if opening_row else 0
stock_value = opening_row.get("stock_value") if opening_row else 0
return opening_row, actual_qty, stock_value
opening_row = get_opening_balance(filters, columns, sl_entries)
return opening_row
def process_stock_ledger_entries(
filters, sl_entries, item_details, opening_row, actual_qty, stock_value, precision
):
def process_stock_ledger_entries(sl_entries, item_details, opening_row, precision):
data = []
conversion_factors = []
if opening_row:
data.append(opening_row)
conversion_factors.append(0)
batch_balance_dict = frappe._dict({})
available_serial_nos = {}
if sabb_list := [sle.serial_and_batch_bundle for sle in sl_entries if sle.serial_and_batch_bundle]:
available_serial_nos = get_serial_nos_from_sle_list(sabb_list)
if actual_qty and filters.get("batch_no"):
batch_balance_dict[filters.batch_no] = [actual_qty, stock_value]
available_serial_nos = get_serial_nos_from_sle_list(
[sle.serial_and_batch_bundle for sle in sl_entries if sle.serial_and_batch_bundle]
)
if not available_serial_nos:
return [], []
for sle in sl_entries:
update_stock_ledger_entry(
sle, item_details, filters, actual_qty, stock_value, batch_balance_dict, precision
)
update_stock_ledger_entry(sle, item_details, precision)
update_available_serial_nos(available_serial_nos, sle)
data.append(sle)
if filters.get("include_uom"):
conversion_factors.append(item_details[sle.item_code].conversion_factor)
return data, conversion_factors
return data
def update_stock_ledger_entry(
sle, item_details, filters, actual_qty, stock_value, batch_balance_dict, precision
):
def update_stock_ledger_entry(sle, item_details, precision):
item_detail = item_details[sle.item_code]
sle.update(item_detail)
if filters.get("batch_no") or check_inventory_dimension_filters_applied(filters):
actual_qty += flt(sle.actual_qty, precision)
stock_value += sle.stock_value_difference
if sle.batch_no:
batch_balance_dict.setdefault(sle.batch_no, [0, 0])
batch_balance_dict[sle.batch_no][0] += sle.actual_qty
if sle.voucher_type == "Stock Reconciliation" and not sle.actual_qty:
actual_qty = sle.qty_after_transaction
stock_value = sle.stock_value
sle.update({"qty_after_transaction": actual_qty, "stock_value": stock_value})
sle.update({"in_qty": max(sle.actual_qty, 0), "out_qty": min(sle.actual_qty, 0)})
if sle.actual_qty:
@@ -120,13 +74,10 @@ def update_available_serial_nos(available_serial_nos, sle):
else available_serial_nos.get(sle.serial_and_batch_bundle)
)
key = (sle.item_code, sle.warehouse)
sle.serial_no = "\n".join(serial_nos) if serial_nos else ""
if key not in available_serial_nos:
stock_balance = get_stock_balance_for(
sle.item_code, sle.warehouse, sle.posting_date, sle.posting_time
)
serials = get_serial_nos(stock_balance["serial_nos"]) if stock_balance["serial_nos"] else []
available_serial_nos.setdefault(key, serials)
sle.balance_serial_no = "\n".join(serials)
available_serial_nos.setdefault(key, serial_nos)
sle.balance_serial_no = "\n".join(serial_nos)
return
existing_serial_no = available_serial_nos[key]
@@ -151,25 +102,14 @@ def get_columns(filters):
},
{"label": _("Item Name"), "fieldname": "item_name", "width": 100},
{
"label": _("Stock UOM"),
"label": _("UOM"),
"fieldname": "stock_uom",
"fieldtype": "Link",
"options": "UOM",
"width": 90,
"width": 60,
},
]
for dimension in get_inventory_dimensions():
columns.append(
{
"label": _(dimension.doctype),
"fieldname": dimension.fieldname,
"fieldtype": "Link",
"options": dimension.doctype,
"width": 110,
}
)
columns.extend(
[
{
@@ -201,20 +141,11 @@ def get_columns(filters):
"width": 150,
},
{
"label": _("Item Group"),
"fieldname": "item_group",
"fieldtype": "Link",
"options": "Item Group",
"width": 100,
"label": _("Serial No (In/Out)"),
"fieldname": "serial_no",
"width": 150,
},
{
"label": _("Brand"),
"fieldname": "brand",
"fieldtype": "Link",
"options": "Brand",
"width": 100,
},
{"label": _("Description"), "fieldname": "description", "width": 200},
{"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 150},
{
"label": _("Incoming Rate"),
"fieldname": "incoming_rate",
@@ -257,28 +188,6 @@ def get_columns(filters):
"width": 110,
"options": "Company:company:default_currency",
},
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110},
{
"label": _("Voucher #"),
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"options": "voucher_type",
"width": 100,
},
{
"label": _("Batch"),
"fieldname": "batch_no",
"fieldtype": "Link",
"options": "Batch",
"width": 100,
},
{
"label": _("Serial No"),
"fieldname": "serial_no",
"fieldtype": "Link",
"options": "Serial No",
"width": 100,
},
{
"label": _("Serial and Batch Bundle"),
"fieldname": "serial_and_batch_bundle",
@@ -286,12 +195,12 @@ def get_columns(filters):
"options": "Serial and Batch Bundle",
"width": 100,
},
{"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 100},
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110},
{
"label": _("Project"),
"fieldname": "project",
"fieldtype": "Link",
"options": "Project",
"label": _("Voucher #"),
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"options": "voucher_type",
"width": 100,
},
{
@@ -310,19 +219,8 @@ def get_columns(filters):
def get_items(filters):
item = frappe.qb.DocType("Item")
query = frappe.qb.from_(item).select(item.name).where(item.has_serial_no == 1)
conditions = []
if item_code := filters.get("item_code"):
conditions.append(item.name == item_code)
else:
if brand := filters.get("brand"):
conditions.append(item.brand == brand)
if item_group := filters.get("item_group"):
if condition := get_item_group_condition(item_group, item):
conditions.append(condition)
if conditions:
for condition in conditions:
query = query.where(condition)
query = query.where(item.name == item_code)
return query.run(pluck=True)

View File

@@ -109,6 +109,8 @@ def get_stock_balance(
from erpnext.stock.stock_ledger import get_previous_sle
frappe.has_permission("Item", "read")
if posting_date is None:
posting_date = nowdate()
if posting_time is None:

View File

@@ -5,7 +5,7 @@
{{ doc.name }}
</span>
</div>
<div class="col-xs-2 small"> {{ doc.total_billable_hours }} </div>
<div class="col-xs-2 small"> {{ doc.billing_hours }} </div>
<div class="col-xs-2 small"> {{ doc.project or '' }} </div>
<div class="col-xs-2 small"> {{ doc.sales_invoice or '' }} </div>
<div class="col-xs-2 small"> {{ _(doc.activity_type) }} </div>