mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-16 21:35:09 +00:00
fix: patch conflict
This commit is contained in:
@@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"]},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`",
|
||||
|
||||
@@ -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
|
||||
|
||||
9
erpnext/patches/v15_0/set_grand_total_to_default_mop.py
Normal file
9
erpnext/patches/v15_0/set_grand_total_to_default_mop.py
Normal 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()
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
"""
|
||||
|
||||
@@ -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 || {},
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user