Merge branch 'develop' into bank-trans-party-automatch

This commit is contained in:
Raffael Meyer 2023-06-01 18:03:10 +02:00 committed by GitHub
commit 09872301bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
193 changed files with 28044 additions and 22710 deletions

View File

@ -125,14 +125,27 @@ def validate_expense_against_budget(args, expense_amount=0):
if not args.account:
return
for budget_against in ["project", "cost_center"] + get_accounting_dimensions():
default_dimensions = [
{
"fieldname": "project",
"document_type": "Project",
},
{
"fieldname": "cost_center",
"document_type": "Cost Center",
},
]
for dimension in default_dimensions + get_accounting_dimensions(as_list=False):
budget_against = dimension.get("fieldname")
if (
args.get(budget_against)
and args.account
and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"})
):
doctype = frappe.unscrub(budget_against)
doctype = dimension.get("document_type")
if frappe.get_cached_value("DocType", doctype, "is_tree"):
lft, rgt = frappe.db.get_value(doctype, args.get(budget_against), ["lft", "rgt"])

View File

@ -12,6 +12,7 @@ from frappe.utils import flt, get_link_to_form
import erpnext
from erpnext.accounts.doctype.journal_entry.journal_entry import get_balance_on
from erpnext.accounts.utils import get_currency_precision
from erpnext.setup.utils import get_exchange_rate
@ -170,6 +171,15 @@ class ExchangeRateRevaluation(Document):
.run(as_dict=True)
)
# round off balance based on currency precision
currency_precision = get_currency_precision()
for acc in account_details:
acc.balance_in_account_currency = flt(acc.balance_in_account_currency, currency_precision)
acc.balance = flt(acc.balance, currency_precision)
acc.zero_balance = (
True if (acc.balance == 0 or acc.balance_in_account_currency == 0) else False
)
return account_details
@staticmethod

View File

@ -4,6 +4,7 @@
import frappe
from frappe import _
from frappe.query_builder.functions import IfNull, Sum
from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
@ -674,18 +675,22 @@ def get_bin_qty(item_code, warehouse):
def get_pos_reserved_qty(item_code, warehouse):
reserved_qty = frappe.db.sql(
"""select sum(p_item.stock_qty) as qty
from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item
where p.name = p_item.parent
and ifnull(p.consolidated_invoice, '') = ''
and p_item.docstatus = 1
and p_item.item_code = %s
and p_item.warehouse = %s
""",
(item_code, warehouse),
as_dict=1,
)
p_inv = frappe.qb.DocType("POS Invoice")
p_item = frappe.qb.DocType("POS Invoice Item")
reserved_qty = (
frappe.qb.from_(p_inv)
.from_(p_item)
.select(Sum(p_item.qty).as_("qty"))
.where(
(p_inv.name == p_item.parent)
& (IfNull(p_inv.consolidated_invoice, "") == "")
& (p_inv.is_return == 0)
& (p_item.docstatus == 1)
& (p_item.item_code == item_code)
& (p_item.warehouse == warehouse)
)
).run(as_dict=True)
return reserved_qty[0].qty or 0 if reserved_qty else 0

View File

@ -469,7 +469,7 @@
"options": "UOM"
},
{
"description": "If rate is zero them item will be treated as \"Free Item\"",
"description": "If rate is zero then item will be treated as \"Free Item\"",
"fieldname": "free_item_rate",
"fieldtype": "Currency",
"label": "Free Item Rate"
@ -670,4 +670,4 @@
"sort_order": "DESC",
"states": [],
"title_field": "title"
}
}

View File

@ -19,7 +19,7 @@ frappe.listview_settings["Purchase Invoice"] = {
],
get_indicator(doc) {
if (doc.status == "Debit Note Issued") {
return [__(doc.status), "darkgrey", "status,=," + doc.status];
return [__(doc.status), "gray", "status,=," + doc.status];
}
if (

View File

@ -1180,7 +1180,12 @@ class SalesInvoice(SellingController):
if self.is_return:
fixed_asset_gl_entries = get_gl_entries_on_asset_regain(
asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name")
asset,
item.base_net_amount,
item.finance_book,
self.get("doctype"),
self.get("name"),
self.get("posting_date"),
)
asset.db_set("disposal_date", None)
@ -1208,7 +1213,12 @@ class SalesInvoice(SellingController):
asset.reload()
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name")
asset,
item.base_net_amount,
item.finance_book,
self.get("doctype"),
self.get("name"),
self.get("posting_date"),
)
asset.db_set("disposal_date", self.posting_date)

View File

@ -0,0 +1,41 @@
{
"creation": "2023-05-23 09:58:17.235916",
"docstatus": 0,
"doctype": "Form Tour",
"first_document": 0,
"idx": 0,
"include_name_field": 0,
"is_standard": 1,
"modified": "2023-05-23 13:10:56.227127",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
"owner": "Administrator",
"reference_doctype": "Sales Invoice",
"save_on_complete": 1,
"steps": [
{
"description": "Select a customer for whom this invoice is being prepared.",
"fieldname": "customer",
"fieldtype": "Link",
"has_next_condition": 1,
"is_table_field": 0,
"label": "Customer",
"next_step_condition": "eval: doc.customer",
"position": "Right",
"title": "Select Customer"
},
{
"child_doctype": "Sales Invoice Item",
"description": "Select item that you have sold along with quantity and rate.",
"fieldname": "items",
"fieldtype": "Table",
"has_next_condition": 0,
"is_table_field": 0,
"parent_fieldname": "items",
"position": "Top",
"title": "Select Item"
}
],
"title": "Sales Invoice"
}

View File

@ -880,18 +880,21 @@ def get_party_shipping_address(doctype, name):
def get_partywise_advanced_payment_amount(
party_type, posting_date=None, future_payment=0, company=None
party_type, posting_date=None, future_payment=0, company=None, party=None
):
cond = "1=1"
if posting_date:
if future_payment:
cond = "posting_date <= '{0}' OR DATE(creation) <= '{0}' " "".format(posting_date)
cond = "(posting_date <= '{0}' OR DATE(creation) <= '{0}')" "".format(posting_date)
else:
cond = "posting_date <= '{0}'".format(posting_date)
if company:
cond += "and company = {0}".format(frappe.db.escape(company))
if party:
cond += "and party = {0}".format(frappe.db.escape(party))
data = frappe.db.sql(
""" SELECT party, sum({0}) as amount
FROM `tabGL Entry`
@ -903,7 +906,6 @@ def get_partywise_advanced_payment_amount(
),
party_type,
)
if data:
return frappe._dict(data)

View File

@ -31,7 +31,6 @@ class AccountsReceivableSummary(ReceivablePayableReport):
def get_data(self, args):
self.data = []
self.receivables = ReceivablePayableReport(self.filters).run(args)[1]
self.get_party_total(args)
@ -42,6 +41,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.filters.report_date,
self.filters.show_future_payments,
self.filters.company,
party=self.filters.get(scrub(self.party_type)),
)
or {}
)
@ -74,6 +74,9 @@ class AccountsReceivableSummary(ReceivablePayableReport):
row.gl_balance = gl_balance_map.get(party)
row.diff = flt(row.outstanding) - flt(row.gl_balance)
if self.filters.show_future_payments:
row.remaining_balance = flt(row.outstanding) - flt(row.future_amount)
self.data.append(row)
def get_party_total(self, args):
@ -106,6 +109,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
"range4": 0.0,
"range5": 0.0,
"total_due": 0.0,
"future_amount": 0.0,
"sales_person": [],
}
),
@ -151,6 +155,10 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.setup_ageing_columns()
if self.filters.show_future_payments:
self.add_column(label=_("Future Payment Amount"), fieldname="future_amount")
self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance")
if self.party_type == "Customer":
self.add_column(
label=_("Territory"), fieldname="territory", fieldtype="Link", options="Territory"

View File

@ -125,12 +125,14 @@ def get_revenue(data, period_list, include_in_gross=1):
data_to_be_removed = True
while data_to_be_removed:
revenue, data_to_be_removed = remove_parent_with_no_child(revenue, period_list)
revenue = adjust_account(revenue, period_list)
revenue, data_to_be_removed = remove_parent_with_no_child(revenue)
adjust_account_totals(revenue, period_list)
return copy.deepcopy(revenue)
def remove_parent_with_no_child(data, period_list):
def remove_parent_with_no_child(data):
data_to_be_removed = False
for parent in data:
if "is_group" in parent and parent.get("is_group") == 1:
@ -147,16 +149,19 @@ def remove_parent_with_no_child(data, period_list):
return data, data_to_be_removed
def adjust_account(data, period_list, consolidated=False):
leaf_nodes = [item for item in data if item["is_group"] == 0]
def adjust_account_totals(data, period_list):
totals = {}
for node in leaf_nodes:
set_total(node, node["total"], data, totals)
for d in data:
for period in period_list:
key = period if consolidated else period.key
d["total"] = totals[d["account"]]
return data
for d in reversed(data):
if d.get("is_group"):
for period in period_list:
# reset totals for group accounts as totals set by get_data doesn't consider include_in_gross check
d[period.key] = sum(
item[period.key] for item in data if item.get("parent_account") == d.get("account")
)
else:
set_total(d, d["total"], data, totals)
d["total"] = totals[d["account"]]
def set_total(node, value, complete_list, totals):
@ -191,6 +196,9 @@ def get_profit(
if profit_loss[key]:
has_value = True
if not profit_loss.get("total"):
profit_loss["total"] = 0
profit_loss["total"] += profit_loss[key]
if has_value:
return profit_loss
@ -229,6 +237,9 @@ def get_net_profit(
if profit_loss[key]:
has_value = True
if not profit_loss.get("total"):
profit_loss["total"] = 0
profit_loss["total"] += profit_loss[key]
if has_value:
return profit_loss

View File

@ -1,6 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from collections import OrderedDict
import frappe
from frappe import _, qb, scrub
@ -736,7 +737,7 @@ class GrossProfitGenerator(object):
def load_invoice_items(self):
conditions = ""
if self.filters.company:
conditions += " and company = %(company)s"
conditions += " and `tabSales Invoice`.company = %(company)s"
if self.filters.from_date:
conditions += " and posting_date >= %(from_date)s"
if self.filters.to_date:
@ -856,30 +857,30 @@ class GrossProfitGenerator(object):
Turns list of Sales Invoice Items to a tree of Sales Invoices with their Items as children.
"""
parents = []
grouped = OrderedDict()
for row in self.si_list:
if row.parent not in parents:
parents.append(row.parent)
# initialize list with a header row for each new parent
grouped.setdefault(row.parent, [self.get_invoice_row(row)]).append(
row.update(
{"indent": 1.0, "parent_invoice": row.parent, "invoice_or_item": row.item_code}
) # descendant rows will have indent: 1.0 or greater
)
parents_index = 0
for index, row in enumerate(self.si_list):
if parents_index < len(parents) and row.parent == parents[parents_index]:
invoice = self.get_invoice_row(row)
self.si_list.insert(index, invoice)
parents_index += 1
# if item is a bundle, add it's components as seperate rows
if frappe.db.exists("Product Bundle", row.item_code):
bundled_items = self.get_bundle_items(row)
for x in bundled_items:
bundle_item = self.get_bundle_item_row(row, x)
grouped.get(row.parent).append(bundle_item)
else:
# skipping the bundle items rows
if not row.indent:
row.indent = 1.0
row.parent_invoice = row.parent
row.invoice_or_item = row.item_code
self.si_list.clear()
if frappe.db.exists("Product Bundle", row.item_code):
self.add_bundle_items(row, index)
for items in grouped.values():
self.si_list.extend(items)
def get_invoice_row(self, row):
# header row format
return frappe._dict(
{
"parent_invoice": "",
@ -908,13 +909,6 @@ class GrossProfitGenerator(object):
}
)
def add_bundle_items(self, product_bundle, index):
bundle_items = self.get_bundle_items(product_bundle)
for i, item in enumerate(bundle_items):
bundle_item = self.get_bundle_item_row(product_bundle, item)
self.si_list.insert((index + i + 1), bundle_item)
def get_bundle_items(self, product_bundle):
return frappe.get_all(
"Product Bundle Item", filters={"parent": product_bundle.item_code}, fields=["item_code", "qty"]

View File

@ -5,8 +5,9 @@
"label": "Profit and Loss"
}
],
"content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Accounts\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Chart of Accounts\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Invoice\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Journal Entry\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Payment Entry\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Receivable\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"General Ledger\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Trial Balance\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounting Masters\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"General Ledger\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Receivable\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Payable\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Financial Statements\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Multi Currency\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Bank Statement\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Subscription Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Share Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Cost Center and Budgeting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Opening and Closing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Taxes\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Profitability\",\"col\":4}}]",
"content": "[{\"id\":\"MmUf9abwxg\",\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Accounts\",\"col\":12}},{\"id\":\"i0EtSjDAXq\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"id\":\"X78jcbq1u3\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"vikWSkNm6_\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"pMywM0nhlj\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Chart of Accounts\",\"col\":3}},{\"id\":\"_pRdD6kqUG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"id\":\"G984SgVRJN\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Invoice\",\"col\":3}},{\"id\":\"1ArNvt9qhz\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Journal Entry\",\"col\":3}},{\"id\":\"F9f4I1viNr\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Payment Entry\",\"col\":3}},{\"id\":\"4IBBOIxfqW\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Receivable\",\"col\":3}},{\"id\":\"El2anpPaFY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"General Ledger\",\"col\":3}},{\"id\":\"1nwcM9upJo\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Trial Balance\",\"col\":3}},{\"id\":\"OF9WOi1Ppc\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"B7-uxs8tkU\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"tHb3yxthkR\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"DnNtsmxpty\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounting Masters\",\"col\":4}},{\"id\":\"nKKr6fjgjb\",\"type\":\"card\",\"data\":{\"card_name\":\"General Ledger\",\"col\":4}},{\"id\":\"xOHTyD8b5l\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Receivable\",\"col\":4}},{\"id\":\"_Cb7C8XdJJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Payable\",\"col\":4}},{\"id\":\"p7NY6MHe2Y\",\"type\":\"card\",\"data\":{\"card_name\":\"Financial Statements\",\"col\":4}},{\"id\":\"KlqilF5R_V\",\"type\":\"card\",\"data\":{\"card_name\":\"Taxes\",\"col\":4}},{\"id\":\"jTUy8LB0uw\",\"type\":\"card\",\"data\":{\"card_name\":\"Cost Center and Budgeting\",\"col\":4}},{\"id\":\"Wn2lhs7WLn\",\"type\":\"card\",\"data\":{\"card_name\":\"Multi Currency\",\"col\":4}},{\"id\":\"PAQMqqNkBM\",\"type\":\"card\",\"data\":{\"card_name\":\"Bank Statement\",\"col\":4}},{\"id\":\"Q_hBCnSeJY\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"3AK1Zf0oew\",\"type\":\"card\",\"data\":{\"card_name\":\"Profitability\",\"col\":4}},{\"id\":\"kxhoaiqdLq\",\"type\":\"card\",\"data\":{\"card_name\":\"Opening and Closing\",\"col\":4}},{\"id\":\"q0MAlU2j_Z\",\"type\":\"card\",\"data\":{\"card_name\":\"Subscription Management\",\"col\":4}},{\"id\":\"ptm7T6Hwu-\",\"type\":\"card\",\"data\":{\"card_name\":\"Share Management\",\"col\":4}},{\"id\":\"OX7lZHbiTr\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
"creation": "2020-03-02 15:41:59.515192",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
@ -1060,10 +1061,11 @@
"type": "Link"
}
],
"modified": "2023-02-23 15:32:12.135355",
"modified": "2023-05-30 13:23:29.316711",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting",
"number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,

View File

@ -41,6 +41,8 @@ frappe.ui.form.on('Asset', {
},
setup: function(frm) {
frm.ignore_doctypes_on_cancel_all = ['Journal Entry'];
frm.make_methods = {
'Asset Movement': () => {
frappe.call({

View File

@ -307,7 +307,7 @@ def scrap_asset(asset_name):
je.company = asset.company
je.remark = "Scrap Entry for asset {0}".format(asset_name)
for entry in get_gl_entries_on_asset_disposal(asset):
for entry in get_gl_entries_on_asset_disposal(asset, date):
entry.update({"reference_type": "Asset", "reference_name": asset_name})
je.append("accounts", entry)
@ -434,8 +434,11 @@ def disposal_happens_in_the_future(posting_date_of_disposal):
def get_gl_entries_on_asset_regain(
asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None
asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None, date=None
):
if not date:
date = getdate()
(
fixed_asset_account,
asset,
@ -453,7 +456,7 @@ def get_gl_entries_on_asset_regain(
"debit_in_account_currency": asset.gross_purchase_amount,
"debit": asset.gross_purchase_amount,
"cost_center": depreciation_cost_center,
"posting_date": getdate(),
"posting_date": date,
},
item=asset,
),
@ -463,7 +466,7 @@ def get_gl_entries_on_asset_regain(
"credit_in_account_currency": accumulated_depr_amount,
"credit": accumulated_depr_amount,
"cost_center": depreciation_cost_center,
"posting_date": getdate(),
"posting_date": date,
},
item=asset,
),
@ -472,7 +475,7 @@ def get_gl_entries_on_asset_regain(
profit_amount = abs(flt(value_after_depreciation)) - abs(flt(selling_amount))
if profit_amount:
get_profit_gl_entries(
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center, date
)
if voucher_type and voucher_no:
@ -484,8 +487,11 @@ def get_gl_entries_on_asset_regain(
def get_gl_entries_on_asset_disposal(
asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None
asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None, date=None
):
if not date:
date = getdate()
(
fixed_asset_account,
asset,
@ -503,7 +509,7 @@ def get_gl_entries_on_asset_disposal(
"credit_in_account_currency": asset.gross_purchase_amount,
"credit": asset.gross_purchase_amount,
"cost_center": depreciation_cost_center,
"posting_date": getdate(),
"posting_date": date,
},
item=asset,
),
@ -513,7 +519,7 @@ def get_gl_entries_on_asset_disposal(
"debit_in_account_currency": accumulated_depr_amount,
"debit": accumulated_depr_amount,
"cost_center": depreciation_cost_center,
"posting_date": getdate(),
"posting_date": date,
},
item=asset,
),
@ -522,7 +528,7 @@ def get_gl_entries_on_asset_disposal(
profit_amount = flt(selling_amount) - flt(value_after_depreciation)
if profit_amount:
get_profit_gl_entries(
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center, date
)
if voucher_type and voucher_no:
@ -556,8 +562,11 @@ def get_asset_details(asset, finance_book=None):
def get_profit_gl_entries(
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center, date=None
):
if not date:
date = getdate()
debit_or_credit = "debit" if profit_amount < 0 else "credit"
gl_entries.append(
asset.get_gl_dict(
@ -566,7 +575,7 @@ def get_profit_gl_entries(
"cost_center": depreciation_cost_center,
debit_or_credit: abs(profit_amount),
debit_or_credit + "_in_account_currency": abs(profit_amount),
"posting_date": getdate(),
"posting_date": date,
},
item=asset,
)

View File

@ -356,6 +356,83 @@ class TestAsset(AssetSetup):
si.cancel()
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
def test_gle_made_by_asset_sale_for_existing_asset(self):
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
asset = create_asset(
calculate_depreciation=1,
available_for_use_date="2020-04-01",
purchase_date="2020-04-01",
expected_value_after_useful_life=0,
total_number_of_depreciations=5,
number_of_depreciations_booked=2,
frequency_of_depreciation=12,
depreciation_start_date="2023-03-31",
opening_accumulated_depreciation=24000,
gross_purchase_amount=60000,
submit=1,
)
expected_depr_values = [
["2023-03-31", 12000, 36000],
["2024-03-31", 12000, 48000],
["2025-03-31", 12000, 60000],
]
first_asset_depr_schedule = get_depr_schedule(asset.name, "Active")
for i, schedule in enumerate(first_asset_depr_schedule):
self.assertEqual(getdate(expected_depr_values[i][0]), schedule.schedule_date)
self.assertEqual(expected_depr_values[i][1], schedule.depreciation_amount)
self.assertEqual(expected_depr_values[i][2], schedule.accumulated_depreciation_amount)
post_depreciation_entries(date="2023-03-31")
si = create_sales_invoice(
item_code="Macbook Pro", asset=asset.name, qty=1, rate=40000, posting_date=getdate("2023-05-23")
)
asset.load_from_db()
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
expected_values = [["2023-03-31", 12000, 36000], ["2023-05-23", 1742.47, 37742.47]]
second_asset_depr_schedule = get_depr_schedule(asset.name, "Active")
for i, schedule in enumerate(second_asset_depr_schedule):
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
self.assertTrue(schedule.journal_entry)
expected_gle = (
(
"_Test Accumulated Depreciations - _TC",
37742.47,
0.0,
),
(
"_Test Fixed Asset - _TC",
0.0,
60000.0,
),
(
"_Test Gain/Loss on Asset Disposal - _TC",
0.0,
17742.47,
),
("Debtors - _TC", 40000.0, 0.0),
)
gle = frappe.db.sql(
"""select account, debit, credit from `tabGL Entry`
where voucher_type='Sales Invoice' and voucher_no = %s
order by account""",
si.name,
)
self.assertSequenceEqual(gle, expected_gle)
def test_asset_with_maintenance_required_status_after_sale(self):
asset = create_asset(
calculate_depreciation=1,

View File

@ -443,6 +443,7 @@ class AssetCapitalization(StockController):
item.get("finance_book") or self.get("finance_book"),
self.get("doctype"),
self.get("name"),
self.get("posting_date"),
)
asset.db_set("disposal_date", self.posting_date)

View File

@ -96,7 +96,6 @@ class AssetCategory(Document):
frappe.throw(msg, title=_("Missing Account"))
@frappe.whitelist()
def get_asset_category_account(
fieldname, item=None, asset=None, account=None, asset_category=None, company=None
):

View File

@ -246,13 +246,12 @@ class AssetDepreciationSchedule(Document):
if should_get_last_day:
schedule_date = get_last_day(schedule_date)
# schedule date will be a year later from start date
# so monthly schedule date is calculated by removing 11 months from it
monthly_schedule_date = add_months(schedule_date, -row.frequency_of_depreciation + 1)
# if asset is being sold or scrapped
if date_of_disposal:
from_date = asset_doc.available_for_use_date
from_date = add_months(
getdate(asset_doc.available_for_use_date),
(asset_doc.number_of_depreciations_booked * row.frequency_of_depreciation),
)
if self.depreciation_schedule:
from_date = self.depreciation_schedule[-1].schedule_date
@ -272,14 +271,20 @@ class AssetDepreciationSchedule(Document):
break
# For first row
if (
(has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
and not self.opening_accumulated_depreciation
and n == 0
):
from_date = add_days(
asset_doc.available_for_use_date, -1
) # needed to calc depr amount for available_for_use_date too
if n == 0 and has_pro_rata and not self.opening_accumulated_depreciation:
from_date = add_days(asset_doc.available_for_use_date, -1)
depreciation_amount, days, months = _get_pro_rata_amt(
row,
depreciation_amount,
from_date,
row.depreciation_start_date,
has_wdv_or_dd_non_yearly_pro_rata,
)
elif n == 0 and has_wdv_or_dd_non_yearly_pro_rata and self.opening_accumulated_depreciation:
from_date = add_months(
getdate(asset_doc.available_for_use_date),
(self.number_of_depreciations_booked * row.frequency_of_depreciation),
)
depreciation_amount, days, months = _get_pro_rata_amt(
row,
depreciation_amount,
@ -287,11 +292,6 @@ class AssetDepreciationSchedule(Document):
row.depreciation_start_date,
has_wdv_or_dd_non_yearly_pro_rata,
)
# For first depr schedule date will be the start date
# so monthly schedule date is calculated by removing
# month difference between use date and start date
monthly_schedule_date = add_months(row.depreciation_start_date, -months + 1)
# For last row
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
@ -316,9 +316,7 @@ class AssetDepreciationSchedule(Document):
depreciation_amount_without_pro_rata, depreciation_amount
)
monthly_schedule_date = add_months(schedule_date, 1)
schedule_date = add_days(schedule_date, days)
last_schedule_date = schedule_date
if not depreciation_amount:
continue

View File

@ -7,12 +7,14 @@
],
"content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Assets\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Asset Value Analytics\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Asset\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Asset Category\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Fixed Asset Register\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Assets\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Maintenance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]",
"creation": "2020-03-02 15:43:27.634865",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "assets",
"idx": 0,
"is_hidden": 0,
"label": "Assets",
"links": [
{
@ -183,13 +185,15 @@
"type": "Link"
}
],
"modified": "2022-01-13 18:25:41.730628",
"modified": "2023-05-24 14:47:20.243146",
"modified_by": "Administrator",
"module": "Assets",
"name": "Assets",
"number_cards": [],
"owner": "Administrator",
"parent_page": "",
"parent_page": "Accounting",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
"sequence_id": 4.0,
@ -216,4 +220,4 @@
}
],
"title": "Assets"
}
}

View File

@ -157,7 +157,7 @@
"party_account_currency",
"inter_company_order_reference",
"is_old_subcontracting_flow",
"dashboard"
"connections_tab"
],
"fields": [
{
@ -1171,7 +1171,6 @@
"depends_on": "is_internal_supplier",
"fieldname": "set_from_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Set From Warehouse",
"options": "Warehouse"
},
@ -1185,12 +1184,6 @@
"fieldtype": "Tab Break",
"label": "More Info"
},
{
"fieldname": "dashboard",
"fieldtype": "Tab Break",
"label": "Dashboard",
"show_dashboard": 1
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
@ -1266,13 +1259,19 @@
"fieldname": "shipping_address_section",
"fieldtype": "Section Break",
"label": "Shipping Address"
},
{
"fieldname": "connections_tab",
"fieldtype": "Tab Break",
"label": "Connections",
"show_dashboard": 1
}
],
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2023-05-07 20:18:09.196799",
"modified": "2023-05-24 11:16:41.195340",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",

View File

@ -7,12 +7,14 @@
],
"content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Buying\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Purchase Order Trends\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Material Request\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Order\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Analytics\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Order Analysis\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Buying\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Items & Pricing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Supplier\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Supplier Scorecard\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Regional\",\"col\":4}}]",
"creation": "2020-01-28 11:50:26.195467",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "buying",
"idx": 0,
"is_hidden": 0,
"label": "Buying",
"links": [
{
@ -509,16 +511,18 @@
"type": "Link"
}
],
"modified": "2022-01-13 17:26:39.090190",
"modified": "2023-05-24 14:47:20.535772",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying",
"number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
"sequence_id": 6.0,
"sequence_id": 5.0,
"shortcuts": [
{
"color": "Green",

View File

@ -2444,7 +2444,7 @@ def set_order_defaults(
Returns a Sales/Purchase Order Item child item containing the default values
"""
p_doc = frappe.get_doc(parent_doctype, parent_doctype_name)
child_item = frappe.new_doc(child_doctype, p_doc, child_docname)
child_item = frappe.new_doc(child_doctype, parent_doc=p_doc, parentfield=child_docname)
item = frappe.get_doc("Item", trans_item.get("item_code"))
for field in ("item_code", "item_name", "description", "item_group"):
@ -2826,6 +2826,17 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent.update_billing_percentage()
parent.set_status()
# Cancel and Recreate Stock Reservation Entries.
if parent_doctype == "Sales Order":
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
cancel_stock_reservation_entries,
has_reserved_stock,
)
if has_reserved_stock(parent.doctype, parent.name):
cancel_stock_reservation_entries(parent.doctype, parent.name)
parent.create_stock_reservation_entries()
@erpnext.allow_regional
def validate_regional(doc):

View File

@ -180,6 +180,7 @@ class BuyingController(SubcontractingController):
address_dict = {
"supplier_address": "address_display",
"shipping_address": "shipping_address_display",
"billing_address": "billing_address_display",
}
for address_field, address_display_field in address_dict.items():

View File

@ -689,7 +689,6 @@ class SubcontractingController(StockController):
"actual_qty": flt(item.rejected_qty) * flt(item.conversion_factor),
"serial_no": cstr(item.rejected_serial_no).strip(),
"incoming_rate": 0.0,
"recalculate_rate": 1,
},
)
)

View File

@ -5,7 +5,7 @@ frappe.ui.form.on('LinkedIn Settings', {
onload: function(frm) {
if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret) {
frappe.confirm(
__('Session not valid, Do you want to login?'),
__('Session not valid. Do you want to login?'),
function(){
frm.trigger("login");
},
@ -14,11 +14,11 @@ frappe.ui.form.on('LinkedIn Settings', {
}
);
}
frm.dashboard.set_headline(__("For more information, {0}.", [`<a target='_blank' href='https://docs.erpnext.com/docs/user/manual/en/CRM/linkedin-settings'>${__('Click here')}</a>`]));
frm.dashboard.set_headline(__("For more information, {0}.", [`<a target='_blank' href='https://docs.erpnext.com/docs/user/manual/en/CRM/linkedin-settings'>${__('click here')}</a>`]));
},
refresh: function(frm) {
if (frm.doc.session_status=="Expired"){
let msg = __("Session Not Active. Save doc to login.");
let msg = __("Session not active. Save document to login.");
frm.dashboard.set_headline_alert(
`<div class="row">
<div class="col-xs-12">
@ -37,7 +37,7 @@ frappe.ui.form.on('LinkedIn Settings', {
let msg,color;
if (days>0){
msg = __("Your Session will be expire in {0} days.", [days]);
msg = __("Your session will be expire in {0} days.", [days]);
color = "green";
}
else {

View File

@ -14,7 +14,7 @@ frappe.ui.form.on('Twitter Settings', {
}
);
}
frm.dashboard.set_headline(__("For more information, {0}.", [`<a target='_blank' href='https://docs.erpnext.com/docs/user/manual/en/CRM/twitter-settings'>${__('Click here')}</a>`]));
frm.dashboard.set_headline(__("For more information, {0}.", [`<a target='_blank' href='https://docs.erpnext.com/docs/user/manual/en/CRM/twitter-settings'>${__('click here')}</a>`]));
},
refresh: function(frm) {
let msg, color, flag=false;

View File

@ -5,177 +5,18 @@
"label": "Territory Wise Sales"
}
],
"content": "[{\"type\":\"chart\",\"data\":{\"chart_name\":\"Territory Wise Sales\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Lead\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Opportunity\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Analytics\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Sales Pipeline\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Campaign\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Maintenance\",\"col\":4}}]",
"content": "[{\"id\":\"Cj2TyhgiWy\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Territory Wise Sales\",\"col\":12}},{\"id\":\"LAKRmpYMRA\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"XGIwEUStw_\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"69RN0XsiJK\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Lead\",\"col\":3}},{\"id\":\"t6PQ0vY-Iw\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Opportunity\",\"col\":3}},{\"id\":\"VOFE0hqXRD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":3}},{\"id\":\"0ik53fuemG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Analytics\",\"col\":3}},{\"id\":\"wdROEmB_XG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"-I9HhcgUKE\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"ttpROKW9vk\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"-76QPdbBHy\",\"type\":\"card\",\"data\":{\"card_name\":\"Sales Pipeline\",\"col\":4}},{\"id\":\"_YmGwzVWRr\",\"type\":\"card\",\"data\":{\"card_name\":\"Masters\",\"col\":4}},{\"id\":\"Bma1PxoXk3\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"80viA0R83a\",\"type\":\"card\",\"data\":{\"card_name\":\"Campaign\",\"col\":4}},{\"id\":\"Buo5HtKRFN\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"id\":\"sLS_x4FMK2\",\"type\":\"card\",\"data\":{\"card_name\":\"Maintenance\",\"col\":4}}]",
"creation": "2020-01-23 14:48:30.183272",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "crm",
"idx": 0,
"is_hidden": 0,
"label": "CRM",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Sales Pipeline",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Lead",
"link_count": 0,
"link_to": "Lead",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Opportunity",
"link_count": 0,
"link_to": "Opportunity",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Prospect",
"link_count": 0,
"link_to": "Prospect",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Customer",
"link_count": 0,
"link_to": "Customer",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Contact",
"link_count": 0,
"link_to": "Contact",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Communication",
"link_count": 0,
"link_to": "Communication",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Contract",
"link_count": 0,
"link_to": "Contract",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Appointment",
"link_count": 0,
"link_to": "Appointment",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Newsletter",
"link_count": 0,
"link_to": "Newsletter",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Lead Source",
"link_count": 0,
"link_to": "Lead Source",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Territory",
"link_count": 0,
"link_to": "Territory",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Customer Group",
"link_count": 0,
"link_to": "Customer Group",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Sales Person",
"link_count": 0,
"link_to": "Sales Person",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Sales Stage",
"link_count": 0,
"link_to": "Sales Stage",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
@ -446,19 +287,183 @@
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Masters",
"link_count": 7,
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Territory",
"link_count": 0,
"link_to": "Territory",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Customer Group",
"link_count": 0,
"link_to": "Customer Group",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Contact",
"link_count": 0,
"link_to": "Contact",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Prospect",
"link_count": 0,
"link_to": "Prospect",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Sales Person",
"link_count": 0,
"link_to": "Sales Person",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Sales Stage",
"link_count": 0,
"link_to": "Sales Stage",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Lead Source",
"link_count": 0,
"link_to": "Lead Source",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Sales Pipeline",
"link_count": 7,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Lead",
"link_count": 0,
"link_to": "Lead",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Opportunity",
"link_count": 0,
"link_to": "Opportunity",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Customer",
"link_count": 0,
"link_to": "Customer",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Contract",
"link_count": 0,
"link_to": "Contract",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Appointment",
"link_count": 0,
"link_to": "Appointment",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Newsletter",
"link_count": 0,
"link_to": "Newsletter",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Communication",
"link_count": 0,
"link_to": "Communication",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
"modified": "2022-07-22 15:03:30.755417",
"modified": "2023-05-26 16:49:04.298122",
"modified_by": "Administrator",
"module": "CRM",
"name": "CRM",
"number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
"sequence_id": 7.0,
"sequence_id": 10.0,
"shortcuts": [
{
"color": "Blue",

View File

@ -1,30 +1,185 @@
{
"charts": [],
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Marketplace\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Payments\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
"content": "[{\"id\":\"e88ADOJ7WC\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Integrations</b></span>\",\"col\":12}},{\"id\":\"G0tyx9WOfm\",\"type\":\"card\",\"data\":{\"card_name\":\"Backup\",\"col\":4}},{\"id\":\"nu4oSjH5Rd\",\"type\":\"card\",\"data\":{\"card_name\":\"Authentication\",\"col\":4}},{\"id\":\"nG8cdkpzoc\",\"type\":\"card\",\"data\":{\"card_name\":\"Google Services\",\"col\":4}},{\"id\":\"4hwuQn6E95\",\"type\":\"card\",\"data\":{\"card_name\":\"Communication Channels\",\"col\":4}},{\"id\":\"sEGAzTJRmq\",\"type\":\"card\",\"data\":{\"card_name\":\"Payments\",\"col\":4}},{\"id\":\"ZC6xu-cLBR\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
"creation": "2020-08-20 19:30:48.138801",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "integration",
"idx": 0,
"is_hidden": 0,
"label": "ERPNext Integrations",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Marketplace",
"link_count": 0,
"label": "Backup",
"link_count": 3,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Woocommerce Settings",
"label": "Dropbox Settings",
"link_count": 0,
"link_to": "Woocommerce Settings",
"link_to": "Dropbox Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "S3 Backup Settings",
"link_count": 0,
"link_to": "S3 Backup Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Google Drive",
"link_count": 0,
"link_to": "Google Drive",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Authentication",
"link_count": 4,
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Social Login",
"link_count": 0,
"link_to": "Social Login Key",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "LDAP Settings",
"link_count": 0,
"link_to": "LDAP Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "OAuth Client",
"link_count": 0,
"link_to": "OAuth Client",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "OAuth Provider Settings",
"link_count": 0,
"link_to": "OAuth Provider Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Communication Channels",
"link_count": 3,
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Webhook",
"link_count": 0,
"link_to": "Webhook",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "SMS Settings",
"link_count": 0,
"link_to": "SMS Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Slack Webhook URL",
"link_count": 0,
"link_to": "Slack Webhook URL",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Google Services",
"link_count": 4,
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Google Settings",
"link_count": 0,
"link_to": "Google Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Google Contacts",
"link_count": 0,
"link_to": "Google Contacts",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Google Calendar",
"link_count": 0,
"link_to": "Google Calendar",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Google Drive",
"link_count": 0,
"link_to": "Google Drive",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
@ -33,12 +188,11 @@
"hidden": 0,
"is_query_report": 0,
"label": "Payments",
"link_count": 0,
"link_count": 3,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "GoCardless Settings",
@ -49,10 +203,9 @@
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "M-Pesa Settings",
"label": "Mpesa Settings",
"link_count": 0,
"link_to": "Mpesa Settings",
"link_type": "DocType",
@ -60,15 +213,6 @@
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Settings",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Plaid Settings",
@ -78,6 +222,14 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Settings",
"link_count": 2,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
@ -88,18 +240,30 @@
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Woocommerce Settings",
"link_count": 0,
"link_to": "Woocommerce Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
"modified": "2022-01-13 17:35:35.508718",
"modified": "2023-05-24 14:47:25.984717",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "ERPNext Integrations",
"number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
"sequence_id": 10.0,
"sequence_id": 21.0,
"shortcuts": [],
"title": "ERPNext Integrations"
}
}

View File

@ -74,13 +74,7 @@ update_website_context = [
my_account_context = "erpnext.e_commerce.shopping_cart.utils.update_my_account_context"
webform_list_context = "erpnext.controllers.website_list_for_contact.get_webform_list_context"
calendars = [
"Task",
"Work Order",
"Leave Application",
"Sales Order",
"Holiday List",
]
calendars = ["Task", "Work Order", "Leave Application", "Sales Order", "Holiday List", "ToDo"]
website_generators = ["Item Group", "Website Item", "BOM", "Sales Partner"]

View File

@ -2,6 +2,7 @@
"charts": [],
"content": "[{\"id\":\"_38WStznya\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"t7o_K__1jB\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Loan Application\",\"col\":3}},{\"id\":\"IRiNDC6w1p\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Loan\",\"col\":3}},{\"id\":\"xbbo0FYbq0\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"7ZL4Bro-Vi\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yhyioTViZ3\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"oYFn4b1kSw\",\"type\":\"card\",\"data\":{\"card_name\":\"Loan\",\"col\":4}},{\"id\":\"vZepJF5tl9\",\"type\":\"card\",\"data\":{\"card_name\":\"Loan Processes\",\"col\":4}},{\"id\":\"k-393Mjhqe\",\"type\":\"card\",\"data\":{\"card_name\":\"Disbursement and Repayment\",\"col\":4}},{\"id\":\"6crJ0DBiBJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Loan Security\",\"col\":4}},{\"id\":\"Um5YwxVLRJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]",
"creation": "2020-03-12 16:35:55.299820",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
@ -279,17 +280,18 @@
"type": "Link"
}
],
"modified": "2023-01-31 19:47:13.114415",
"modified": "2023-05-24 14:47:24.109945",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loans",
"number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
"sequence_id": 16.0,
"sequence_id": 15.0,
"shortcuts": [
{
"color": "Green",

View File

@ -439,7 +439,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2022-11-09 15:02:44.490731",
"modified": "2023-05-23 09:56:43.826602",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card",

View File

@ -653,23 +653,19 @@ class JobCard(Document):
exc=JobCardOverTransferError,
)
job_card_items_transferred_qty = _get_job_card_items_transferred_qty(ste_doc)
job_card_items_transferred_qty = _get_job_card_items_transferred_qty(ste_doc) or {}
allow_excess = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer")
if job_card_items_transferred_qty:
allow_excess = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer")
for row in ste_doc.items:
if not row.job_card_item:
continue
for row in ste_doc.items:
if not row.job_card_item:
continue
transferred_qty = flt(job_card_items_transferred_qty.get(row.job_card_item, 0.0))
transferred_qty = flt(job_card_items_transferred_qty.get(row.job_card_item))
if not allow_excess:
_validate_over_transfer(row, transferred_qty)
if not allow_excess:
_validate_over_transfer(row, transferred_qty)
frappe.db.set_value(
"Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty)
)
frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty))
def set_transferred_qty(self, update_status=False):
"Set total FG Qty in Job Card for which RM was transferred."
@ -730,7 +726,7 @@ class JobCard(Document):
self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0]
if self.docstatus < 2:
if self.for_quantity <= self.transferred_qty:
if flt(self.for_quantity) <= flt(self.transferred_qty):
self.status = "Material Transferred"
if self.time_logs:

View File

@ -7,13 +7,14 @@ from typing import Literal
import frappe
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import random_string
from frappe.utils.data import add_to_date, now
from frappe.utils.data import add_to_date, now, today
from erpnext.manufacturing.doctype.job_card.job_card import (
JobCardOverTransferError,
OperationMismatchError,
OverlapError,
make_corrective_job_card,
make_material_request,
)
from erpnext.manufacturing.doctype.job_card.job_card import (
make_stock_entry as make_stock_entry_from_jc,
@ -342,6 +343,12 @@ class TestJobCard(FrappeTestCase):
job_card.reload()
self.assertEqual(job_card.transferred_qty, 2)
transfer_entry_2.cancel()
transfer_entry.cancel()
job_card.reload()
self.assertEqual(job_card.transferred_qty, 0.0)
def test_job_card_material_transfer_correctness(self):
"""
1. Test if only current Job Card Items are pulled in a Stock Entry against a Job Card
@ -443,6 +450,25 @@ class TestJobCard(FrappeTestCase):
jc.docstatus = 2
assertStatus("Cancelled")
def test_job_card_material_request_and_bom_details(self):
from erpnext.stock.doctype.material_request.material_request import make_stock_entry
create_bom_with_multiple_operations()
work_order = make_wo_with_transfer_against_jc()
job_card_name = frappe.db.get_value("Job Card", {"work_order": work_order.name}, "name")
mr = make_material_request(job_card_name)
mr.schedule_date = today()
mr.submit()
ste = make_stock_entry(mr.name)
self.assertEqual(ste.purpose, "Material Transfer for Manufacture")
self.assertEqual(ste.work_order, work_order.name)
self.assertEqual(ste.job_card, job_card_name)
self.assertEqual(ste.from_bom, 1.0)
self.assertEqual(ste.bom_no, work_order.bom_no)
def create_bom_with_multiple_operations():
"Create a BOM with multiple operations and Material Transfer against Job Card"

View File

@ -451,10 +451,14 @@ frappe.ui.form.on("Material Request Plan Item", {
for_warehouse: row.warehouse
},
callback: function(r) {
let {projected_qty, actual_qty} = r.message;
if (r.message) {
let {projected_qty, actual_qty} = r.message[0];
frappe.model.set_value(cdt, cdn, 'projected_qty', projected_qty);
frappe.model.set_value(cdt, cdn, 'actual_qty', actual_qty);
frappe.model.set_value(cdt, cdn, {
'projected_qty': projected_qty,
'actual_qty': actual_qty
});
}
}
})
}

View File

@ -35,8 +35,12 @@
"section_break_25",
"prod_plan_references",
"section_break_24",
"get_sub_assembly_items",
"combine_sub_items",
"section_break_ucc4",
"skip_available_sub_assembly_item",
"column_break_igxl",
"get_sub_assembly_items",
"section_break_g4ip",
"sub_assembly_items",
"download_materials_request_plan_section_section",
"download_materials_required",
@ -351,12 +355,12 @@
{
"fieldname": "section_break_24",
"fieldtype": "Section Break",
"hide_border": 1
"hide_border": 1,
"label": "Sub Assembly Items"
},
{
"fieldname": "sub_assembly_items",
"fieldtype": "Table",
"label": "Sub Assembly Items",
"no_copy": 1,
"options": "Production Plan Sub Assembly Item"
},
@ -392,13 +396,33 @@
"fieldname": "download_materials_request_plan_section_section",
"fieldtype": "Section Break",
"label": "Download Materials Request Plan Section"
},
{
"default": "0",
"description": "System consider the projected quantity to check available or will be available sub-assembly items ",
"fieldname": "skip_available_sub_assembly_item",
"fieldtype": "Check",
"label": "Skip Available Sub Assembly Items"
},
{
"fieldname": "section_break_ucc4",
"fieldtype": "Column Break",
"hide_border": 1
},
{
"fieldname": "section_break_g4ip",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_igxl",
"fieldtype": "Column Break"
}
],
"icon": "fa fa-calendar",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-03-31 10:30:48.118932",
"modified": "2023-05-22 23:36:31.770517",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",

View File

@ -718,7 +718,9 @@ class ProductionPlan(Document):
frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx))
bom_data = []
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty)
warehouse = row.warehouse if self.skip_available_sub_assembly_item else None
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty, self.company, warehouse=warehouse)
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
sub_assembly_items_store.extend(bom_data)
@ -894,7 +896,9 @@ def download_raw_materials(doc, warehouses=None):
build_csv_response(item_list, doc.name)
def get_exploded_items(item_details, company, bom_no, include_non_stock_items, planned_qty=1):
def get_exploded_items(
item_details, company, bom_no, include_non_stock_items, planned_qty=1, doc=None
):
bei = frappe.qb.DocType("BOM Explosion Item")
bom = frappe.qb.DocType("BOM")
item = frappe.qb.DocType("Item")
@ -1271,6 +1275,12 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
include_safety_stock = doc.get("include_safety_stock")
so_item_details = frappe._dict()
sub_assembly_items = {}
if doc.get("skip_available_sub_assembly_item"):
for d in doc.get("sub_assembly_items"):
sub_assembly_items.setdefault((d.get("production_item"), d.get("bom_no")), d.get("qty"))
for data in po_items:
if not data.get("include_exploded_items") and doc.get("sub_assembly_items"):
data["include_exploded_items"] = 1
@ -1296,10 +1306,24 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get("idx")))
if bom_no:
if data.get("include_exploded_items") and include_subcontracted_items:
if (
data.get("include_exploded_items")
and doc.get("sub_assembly_items")
and doc.get("skip_available_sub_assembly_item")
):
item_details = get_raw_materials_of_sub_assembly_items(
item_details,
company,
bom_no,
include_non_stock_items,
sub_assembly_items,
planned_qty=planned_qty,
)
elif data.get("include_exploded_items") and include_subcontracted_items:
# fetch exploded items from BOM
item_details = get_exploded_items(
item_details, company, bom_no, include_non_stock_items, planned_qty=planned_qty
item_details, company, bom_no, include_non_stock_items, planned_qty=planned_qty, doc=doc
)
else:
item_details = get_subitems(
@ -1456,12 +1480,22 @@ def get_item_data(item_code):
}
def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, company, warehouse=None, indent=0):
data = get_bom_children(parent=bom_no)
for d in data:
if d.expandable:
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
if warehouse:
bin_dict = get_bin_details(d, company, for_warehouse=warehouse)
if bin_dict and bin_dict[0].projected_qty > 0:
if bin_dict[0].projected_qty > stock_qty:
continue
else:
stock_qty = stock_qty - bin_dict[0].projected_qty
bom_data.append(
frappe._dict(
{
@ -1481,7 +1515,7 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
)
if d.value:
get_sub_assembly_items(d.value, bom_data, stock_qty, indent=indent + 1)
get_sub_assembly_items(d.value, bom_data, stock_qty, company, warehouse, indent=indent + 1)
def set_default_warehouses(row, default_warehouses):
@ -1500,7 +1534,7 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
frappe.qb.from_(table)
.inner_join(child)
.on(table.name == child.parent)
.select(Sum(child.quantity * IfNull(child.conversion_factor, 1.0)))
.select(Sum(child.required_bom_qty * IfNull(child.conversion_factor, 1.0)))
.where(
(table.docstatus == 1)
& (child.item_code == item_code)
@ -1518,4 +1552,72 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
get_reserved_qty_for_production(item_code, warehouse, check_production_plan=True)
)
if reserved_qty_for_production > reserved_qty_for_production_plan:
return 0.0
return reserved_qty_for_production_plan - reserved_qty_for_production
def get_raw_materials_of_sub_assembly_items(
item_details, company, bom_no, include_non_stock_items, sub_assembly_items, planned_qty=1
):
bei = frappe.qb.DocType("BOM Item")
bom = frappe.qb.DocType("BOM")
item = frappe.qb.DocType("Item")
item_default = frappe.qb.DocType("Item Default")
item_uom = frappe.qb.DocType("UOM Conversion Detail")
items = (
frappe.qb.from_(bei)
.join(bom)
.on(bom.name == bei.parent)
.join(item)
.on(item.name == bei.item_code)
.left_join(item_default)
.on((item_default.parent == item.name) & (item_default.company == company))
.left_join(item_uom)
.on((item.name == item_uom.parent) & (item_uom.uom == item.purchase_uom))
.select(
(IfNull(Sum(bei.stock_qty / IfNull(bom.quantity, 1)), 0) * planned_qty).as_("qty"),
item.item_name,
item.name.as_("item_code"),
bei.description,
bei.stock_uom,
bei.bom_no,
item.min_order_qty,
bei.source_warehouse,
item.default_material_request_type,
item.min_order_qty,
item_default.default_warehouse,
item.purchase_uom,
item_uom.conversion_factor,
item.safety_stock,
)
.where(
(bei.docstatus == 1)
& (bom.name == bom_no)
& (item.is_stock_item.isin([0, 1]) if include_non_stock_items else item.is_stock_item == 1)
)
.groupby(bei.item_code, bei.stock_uom)
).run(as_dict=True)
for item in items:
key = (item.item_code, item.bom_no)
if item.bom_no and key in sub_assembly_items:
planned_qty = flt(sub_assembly_items[key])
get_raw_materials_of_sub_assembly_items(
item_details,
company,
item.bom_no,
include_non_stock_items,
sub_assembly_items,
planned_qty=planned_qty,
)
else:
if not item.conversion_factor and item.purchase_uom:
item.conversion_factor = get_uom_conversion_factor(item.item_code, item.purchase_uom)
item_details.setdefault(item.get("item_code"), item)
return item_details

View File

@ -926,6 +926,50 @@ class TestProductionPlan(FrappeTestCase):
self.assertEqual(after_qty, before_qty)
def test_skip_available_qty_for_sub_assembly_items(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
bom_tree = {
"Fininshed Goods1 For SUB Test": {
"SubAssembly1 For SUB Test": {"ChildPart1 For SUB Test": {}},
"SubAssembly2 For SUB Test": {},
}
}
parent_bom = create_nested_bom(bom_tree, prefix="")
plan = create_production_plan(
item_code=parent_bom.item,
planned_qty=10,
ignore_existing_ordered_qty=1,
do_not_submit=1,
skip_available_sub_assembly_item=1,
warehouse="_Test Warehouse - _TC",
)
make_stock_entry(
item_code="SubAssembly1 For SUB Test",
qty=5,
rate=100,
target="_Test Warehouse - _TC",
)
self.assertTrue(plan.skip_available_sub_assembly_item)
plan.get_sub_assembly_items()
for row in plan.sub_assembly_items:
if row.production_item == "SubAssembly1 For SUB Test":
self.assertEqual(row.qty, 5)
mr_items = get_items_for_material_requests(plan.as_dict())
for row in mr_items:
row = frappe._dict(row)
if row.item_code == "ChildPart1 For SUB Test":
self.assertEqual(row.quantity, 5)
if row.item_code == "SubAssembly2 For SUB Test":
self.assertEqual(row.quantity, 10)
def create_production_plan(**args):
"""
@ -945,6 +989,7 @@ def create_production_plan(**args):
"include_subcontracted_items": args.include_subcontracted_items or 0,
"ignore_existing_ordered_qty": args.ignore_existing_ordered_qty or 0,
"get_items_from": "Sales Order",
"skip_available_sub_assembly_item": args.skip_available_sub_assembly_item or 0,
}
)
@ -958,6 +1003,7 @@ def create_production_plan(**args):
"planned_qty": args.planned_qty or 1,
"planned_start_date": args.planned_start_date or now_datetime(),
"stock_uom": args.stock_uom or "Nos",
"warehouse": args.warehouse,
},
)

View File

@ -28,7 +28,11 @@
"uom",
"stock_uom",
"column_break_22",
"description"
"description",
"section_break_4rxf",
"actual_qty",
"column_break_xfhm",
"projected_qty"
],
"fields": [
{
@ -183,12 +187,34 @@
"fieldtype": "Datetime",
"in_list_view": 1,
"label": "Schedule Date"
},
{
"fieldname": "section_break_4rxf",
"fieldtype": "Section Break"
},
{
"fieldname": "actual_qty",
"fieldtype": "Float",
"label": "Actual Qty",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "column_break_xfhm",
"fieldtype": "Column Break"
},
{
"fieldname": "projected_qty",
"fieldtype": "Float",
"label": "Projected Qty",
"no_copy": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-11-28 13:50:15.116082",
"modified": "2023-05-22 17:52:34.708879",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Sub Assembly Item",

View File

@ -1,13 +1,15 @@
{
"charts": [],
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
"content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
"creation": "2020-03-02 17:11:37.032604",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "organization",
"idx": 0,
"is_hidden": 0,
"label": "Manufacturing",
"links": [
{
@ -243,7 +245,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Bill of Materials",
"link_count": 15,
"link_count": 6,
"onboard": 0,
"type": "Card Break"
},
@ -312,117 +314,20 @@
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Work Order",
"hidden": 0,
"is_query_report": 1,
"label": "Production Planning Report",
"link_count": 0,
"link_to": "Production Planning Report",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Quality Inspection",
"hidden": 0,
"is_query_report": 1,
"label": "Work Order Summary",
"link_count": 0,
"link_to": "Work Order Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Downtime Entry",
"hidden": 0,
"is_query_report": 1,
"label": "Quality Inspection Summary",
"link_count": 0,
"link_to": "Quality Inspection Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Job Card",
"hidden": 0,
"is_query_report": 1,
"label": "Downtime Analysis",
"link_count": 0,
"link_to": "Downtime Analysis",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "BOM",
"hidden": 0,
"is_query_report": 1,
"label": "Job Card Summary",
"link_count": 0,
"link_to": "Job Card Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "BOM",
"hidden": 0,
"is_query_report": 1,
"label": "BOM Search",
"link_count": 0,
"link_to": "BOM Search",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Work Order",
"hidden": 0,
"is_query_report": 1,
"label": "BOM Stock Report",
"link_count": 0,
"link_to": "BOM Stock Report",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "BOM",
"hidden": 0,
"is_query_report": 1,
"label": "Production Analytics",
"link_count": 0,
"link_to": "Production Analytics",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "BOM Operations Time",
"link_count": 0,
"link_to": "BOM Operations Time",
"link_type": "Report",
"onboard": 0,
"type": "Link"
}
],
"modified": "2022-11-14 14:53:34.616862",
"modified": "2023-05-27 16:41:04.776115",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing",
"number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
"sequence_id": 17.0,
"sequence_id": 8.0,
"shortcuts": [
{
"color": "Grey",

View File

@ -326,10 +326,12 @@ erpnext.patches.v13_0.update_docs_link
erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries
erpnext.patches.v15_0.update_gpa_and_ndb_for_assdeprsch
erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance
erpnext.patches.v14_0.update_closing_balances #10-05-2023
erpnext.patches.v14_0.update_closing_balances #17-05-2023
execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
# below migration patches should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
execute:frappe.delete_doc_if_exists("Report", "Tax Detail")
erpnext.patches.v15_0.enable_all_leads
erpnext.patches.v14_0.update_company_in_ldc
erpnext.patches.v14_0.set_packed_qty_in_draft_delivery_notes
erpnext.patches.v14_0.cleanup_workspaces

View File

@ -15,7 +15,7 @@ def execute():
web_item = frappe.db.get_value("Website Item", {"item_code": row.parent})
web_item_doc = frappe.get_doc("Website Item", web_item)
child_doc = frappe.new_doc(docfield.options, web_item_doc, field)
child_doc = frappe.new_doc(docfield.options, parent_doc=web_item_doc, parentfield=field)
for field in ["name", "creation", "modified", "idx"]:
row[field] = None

View File

@ -0,0 +1,9 @@
import frappe
def execute():
for ws in ["Retail", "Utilities"]:
frappe.delete_doc_if_exists("Workspace", ws)
for ws in ["Integrations", "Settings"]:
frappe.db.set_value("Workspace", ws, "public", 0)

View File

@ -0,0 +1,60 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe.query_builder.functions import Sum
def execute():
ps = frappe.qb.DocType("Packing Slip")
dn = frappe.qb.DocType("Delivery Note")
ps_item = frappe.qb.DocType("Packing Slip Item")
ps_details = (
frappe.qb.from_(ps)
.join(ps_item)
.on(ps.name == ps_item.parent)
.join(dn)
.on(ps.delivery_note == dn.name)
.select(
dn.name.as_("delivery_note"),
ps_item.item_code.as_("item_code"),
Sum(ps_item.qty).as_("packed_qty"),
)
.where((ps.docstatus == 1) & (dn.docstatus == 0))
.groupby(dn.name, ps_item.item_code)
).run(as_dict=True)
if ps_details:
dn_list = set()
item_code_list = set()
for ps_detail in ps_details:
dn_list.add(ps_detail.delivery_note)
item_code_list.add(ps_detail.item_code)
dn_item = frappe.qb.DocType("Delivery Note Item")
dn_item_query = (
frappe.qb.from_(dn_item)
.select(
dn.parent.as_("delivery_note"),
dn_item.name,
dn_item.item_code,
dn_item.qty,
)
.where((dn_item.parent.isin(dn_list)) & (dn_item.item_code.isin(item_code_list)))
)
dn_details = frappe._dict()
for r in dn_item_query.run(as_dict=True):
dn_details.setdefault((r.delivery_note, r.item_code), frappe._dict()).setdefault(r.name, r.qty)
for ps_detail in ps_details:
dn_items = dn_details.get((ps_detail.delivery_note, ps_detail.item_code))
if dn_items:
remaining_qty = ps_detail.packed_qty
for name, qty in dn_items.items():
if remaining_qty > 0:
row_packed_qty = min(qty, remaining_qty)
frappe.db.set_value("Delivery Note Item", name, "packed_qty", row_packed_qty)
remaining_qty -= row_packed_qty

View File

@ -13,8 +13,8 @@ from erpnext.accounts.utils import get_fiscal_year
def execute():
frappe.db.truncate("Account Closing Balance")
i = 0
company_wise_order = {}
get_opening_entries = True
for pcv in frappe.db.get_all(
"Period Closing Voucher",
fields=["company", "posting_date", "name"],
@ -29,6 +29,7 @@ def execute():
pcv.posting_date, pcv.fiscal_year, company=pcv.company
)[1]
# get gl entries against pcv
gl_entries = frappe.db.get_all(
"GL Entry", filters={"voucher_no": pcv.name, "is_cancelled": 0}, fields=["*"]
)
@ -37,20 +38,31 @@ def execute():
entry["closing_date"] = pcv_doc.posting_date
entry["period_closing_voucher"] = pcv_doc.name
# get all gl entries for the year
closing_entries = frappe.db.get_all(
"GL Entry",
filters={
"is_cancelled": 0,
"voucher_no": ["!=", pcv.name],
"posting_date": ["<=", pcv.posting_date],
"posting_date": ["between", [pcv_doc.year_start_date, pcv.posting_date]],
"is_opening": "No",
},
fields=["*"],
)
if i == 0:
# add opening entries only for the first pcv
closing_entries += frappe.db.get_all(
"GL Entry",
filters={"is_cancelled": 0, "is_opening": "Yes"},
fields=["*"],
)
for entry in closing_entries:
entry["closing_date"] = pcv_doc.posting_date
entry["period_closing_voucher"] = pcv_doc.name
make_closing_entries(gl_entries + closing_entries, voucher_name=pcv.name)
company_wise_order[pcv.company].append(pcv.posting_date)
get_opening_entries = False
i += 1

View File

@ -234,13 +234,13 @@
{
"fieldname": "actual_start_date",
"fieldtype": "Date",
"label": "Actual Start Date (via Time Sheet)",
"label": "Actual Start Date (via Timesheet)",
"read_only": 1
},
{
"fieldname": "actual_time",
"fieldtype": "Float",
"label": "Actual Time (in Hours via Time Sheet)",
"label": "Actual Time in Hours (via Timesheet)",
"read_only": 1
},
{
@ -250,7 +250,7 @@
{
"fieldname": "actual_end_date",
"fieldtype": "Date",
"label": "Actual End Date (via Time Sheet)",
"label": "Actual End Date (via Timesheet)",
"oldfieldname": "act_completion_date",
"oldfieldtype": "Date",
"read_only": 1
@ -275,7 +275,7 @@
{
"fieldname": "total_costing_amount",
"fieldtype": "Currency",
"label": "Total Costing Amount (via Timesheets)",
"label": "Total Costing Amount (via Timesheet)",
"read_only": 1
},
{
@ -304,19 +304,19 @@
{
"fieldname": "total_billable_amount",
"fieldtype": "Currency",
"label": "Total Billable Amount (via Timesheets)",
"label": "Total Billable Amount (via Timesheet)",
"read_only": 1
},
{
"fieldname": "total_billed_amount",
"fieldtype": "Currency",
"label": "Total Billed Amount (via Sales Invoices)",
"label": "Total Billed Amount (via Sales Invoice)",
"read_only": 1
},
{
"fieldname": "total_consumed_material_cost",
"fieldtype": "Currency",
"label": "Total Consumed Material Cost (via Stock Entry)",
"label": "Total Consumed Material Cost (via Stock Entry)",
"read_only": 1
},
{
@ -451,7 +451,7 @@
"index_web_pages_for_search": 1,
"links": [],
"max_attachments": 4,
"modified": "2023-02-14 04:54:25.819620",
"modified": "2023-04-17 21:11:11.346986",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project",

View File

@ -240,7 +240,7 @@
{
"fieldname": "act_start_date",
"fieldtype": "Date",
"label": "Actual Start Date (via Time Sheet)",
"label": "Actual Start Date (via Timesheet)",
"oldfieldname": "act_start_date",
"oldfieldtype": "Date",
"read_only": 1
@ -248,7 +248,7 @@
{
"fieldname": "actual_time",
"fieldtype": "Float",
"label": "Actual Time (in Hours via Time Sheet)",
"label": "Actual Time in Hours (via Timesheet)",
"read_only": 1
},
{
@ -258,7 +258,7 @@
{
"fieldname": "act_end_date",
"fieldtype": "Date",
"label": "Actual End Date (via Time Sheet)",
"label": "Actual End Date (via Timesheet)",
"oldfieldname": "act_end_date",
"oldfieldtype": "Date",
"read_only": 1
@ -272,7 +272,7 @@
{
"fieldname": "total_costing_amount",
"fieldtype": "Currency",
"label": "Total Costing Amount (via Time Sheet)",
"label": "Total Costing Amount (via Timesheet)",
"oldfieldname": "actual_budget",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
@ -285,7 +285,7 @@
{
"fieldname": "total_billing_amount",
"fieldtype": "Currency",
"label": "Total Billing Amount (via Time Sheet)",
"label": "Total Billable Amount (via Timesheet)",
"read_only": 1
},
{
@ -389,7 +389,7 @@
"is_tree": 1,
"links": [],
"max_attachments": 5,
"modified": "2022-06-23 16:58:47.005241",
"modified": "2023-04-17 21:06:50.174418",
"modified_by": "Administrator",
"module": "Projects",
"name": "Task",

View File

@ -66,8 +66,10 @@ class Task(NestedSet):
task_date = self.get(fieldname)
if task_date and date_diff(project_end_date, getdate(task_date)) < 0:
frappe.throw(
_("Task's {0} cannot be after Project's Expected End Date.").format(
_(self.meta.get_label(fieldname))
_("{0}'s {1} cannot be after {2}'s Expected End Date.").format(
frappe.bold(frappe.get_desk_link("Task", self.name)),
_(self.meta.get_label(fieldname)),
frappe.bold(frappe.get_desk_link("Project", self.project)),
),
frappe.exceptions.InvalidDates,
)

View File

@ -7,12 +7,14 @@
],
"content": "[{\"type\":\"chart\",\"data\":{\"chart_name\":\"Open Projects\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Task\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Project\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Timesheet\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Project Billing Summary\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Projects\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Time Tracking\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
"creation": "2020-03-02 15:46:04.874669",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "project",
"idx": 0,
"is_hidden": 0,
"label": "Projects",
"links": [
{
@ -190,17 +192,18 @@
"type": "Link"
}
],
"modified": "2022-10-11 22:39:10.436311",
"modified": "2023-05-24 14:47:23.179860",
"modified_by": "Administrator",
"module": "Projects",
"name": "Projects",
"number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
"sequence_id": 20.0,
"sequence_id": 11.0,
"shortcuts": [
{
"color": "Blue",

View File

@ -29,7 +29,6 @@
"public/js/help_links.js",
"public/js/agriculture/ternary_plot.js",
"public/js/templates/item_quick_entry.html",
"public/js/utils/item_quick_entry.js",
"public/js/utils/customer_quick_entry.js",
"public/js/utils/supplier_quick_entry.js",
"public/js/education/student_button.html",

View File

@ -494,7 +494,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
},
() => {
// for internal customer instead of pricing rule directly apply valuation rate on item
if (me.frm.doc.is_internal_customer || me.frm.doc.is_internal_supplier) {
if ((me.frm.doc.is_internal_customer || me.frm.doc.is_internal_supplier) && me.frm.doc.represents_company === me.frm.doc.company) {
me.get_incoming_rate(item, me.frm.posting_date, me.frm.posting_time,
me.frm.doc.doctype, me.frm.doc.company);
} else {

View File

@ -12,7 +12,6 @@ import "./utils/item_selector";
import "./help_links";
import "./agriculture/ternary_plot";
import "./templates/item_quick_entry.html";
import "./utils/item_quick_entry";
import "./utils/contact_address_quick_entry";
import "./utils/customer_quick_entry";
import "./utils/supplier_quick_entry";

View File

@ -1,407 +0,0 @@
frappe.provide('frappe.ui.form');
frappe.ui.form.ItemQuickEntryForm = class ItemQuickEntryForm extends frappe.ui.form.QuickEntryForm {
constructor(doctype, after_insert) {
super(doctype, after_insert);
}
render_dialog() {
this.mandatory = this.get_variant_fields().concat(this.mandatory);
this.mandatory = this.mandatory.concat(this.get_attributes_fields());
this.check_naming_series_based_on();
super.render_dialog();
this.init_post_render_dialog_operations();
this.preset_fields_for_template();
this.dialog.$wrapper.find('.edit-full').text(__('Edit in full page for more options like assets, serial nos, batches etc.'))
}
check_naming_series_based_on() {
if (frappe.defaults.get_default("item_naming_by") === "Naming Series") {
this.mandatory = this.mandatory.filter(d => d.fieldname !== "item_code");
}
}
init_post_render_dialog_operations() {
this.dialog.fields_dict.attribute_html.$wrapper.append(frappe.render_template("item_quick_entry"));
this.init_for_create_variant_trigger();
this.init_for_item_template_trigger();
// explicitly hide manufacturing fields as hidden not working.
this.toggle_manufacturer_fields();
this.dialog.get_field("item_template").df.hidden = 1;
this.dialog.get_field("item_template").refresh();
}
register_primary_action() {
var me = this;
this.dialog.set_primary_action(__('Save'), function() {
if (me.dialog.working) return;
var data = me.dialog.get_values();
var variant_values = {};
if (me.dialog.fields_dict.create_variant.$input.prop("checked")) {
variant_values = me.get_variant_doc();
if (!Object.keys(variant_values).length) {
data = null;
}
variant_values.stock_uom = me.template_doc.stock_uom;
variant_values.item_group = me.template_doc.item_group;
}
if (data) {
me.dialog.working = true;
var values = me.update_doc();
//patch for manufacturer type variants as extend is overwriting it.
if (variant_values['variant_based_on'] == "Manufacturer") {
values['variant_based_on'] = "Manufacturer";
}
$.extend(variant_values, values);
me.insert(variant_values);
}
});
}
insert(variant_values) {
let me = this;
return new Promise(resolve => {
frappe.call({
method: "frappe.client.insert",
args: {
doc: variant_values
},
callback: function(r) {
me.dialog.hide();
// delete the old doc
frappe.model.clear_doc(me.dialog.doc.doctype, me.dialog.doc.name);
me.dialog.doc = r.message;
if (frappe._from_link) {
frappe.ui.form.update_calling_link(me.dialog.doc);
} else {
if (me.after_insert) {
me.after_insert(me.dialog.doc);
} else {
me.open_form_if_not_list();
}
}
},
error: function() {
me.open_doc();
},
always: function() {
me.dialog.working = false;
resolve(me.dialog.doc);
},
freeze: true
});
});
}
open_doc() {
this.dialog.hide();
this.update_doc();
if (this.dialog.fields_dict.create_variant.$input.prop("checked")) {
var template = this.dialog.fields_dict.item_template.input.value;
if (template)
frappe.set_route("Form", this.doctype, template);
} else {
frappe.set_route('Form', this.doctype, this.doc.name);
}
}
get_variant_fields() {
var variant_fields = [{
fieldname: "create_variant",
fieldtype: "Check",
label: __("Create Variant")
},
{
fieldname: 'item_template',
label: __('Item Template'),
reqd: 0,
fieldtype: 'Link',
options: "Item",
get_query: function() {
return {
filters: {
"has_variants": 1
}
};
}
}];
return variant_fields;
}
get_manufacturing_fields() {
this.manufacturer_fields = [{
fieldtype: 'Link',
options: 'Manufacturer',
label: 'Manufacturer',
fieldname: "manufacturer",
hidden: 1,
reqd: 0
}, {
fieldtype: 'Data',
label: 'Manufacturer Part Number',
fieldname: 'manufacturer_part_no',
hidden: 1,
reqd: 0
}];
return this.manufacturer_fields;
}
get_attributes_fields() {
var attribute_fields = [{
fieldname: 'attribute_html',
fieldtype: 'HTML'
}]
attribute_fields = attribute_fields.concat(this.get_manufacturing_fields());
return attribute_fields;
}
init_for_create_variant_trigger() {
var me = this;
this.dialog.fields_dict.create_variant.$input.on("click", function() {
me.preset_fields_for_template();
me.init_post_template_trigger_operations(false, [], true);
});
}
preset_fields_for_template() {
var for_variant = this.dialog.get_value('create_variant');
// setup template field, seen and mandatory if variant
let template_field = this.dialog.get_field("item_template");
template_field.df.reqd = for_variant;
template_field.set_value('');
template_field.df.hidden = !for_variant;
template_field.refresh();
// hide properties for variant
['item_code', 'item_name', 'item_group', 'stock_uom'].forEach((d) => {
let f = this.dialog.get_field(d);
f.df.hidden = for_variant;
f.refresh();
});
this.dialog.get_field('attribute_html').toggle(false);
// non mandatory for variants
['item_code', 'stock_uom', 'item_group'].forEach((d) => {
let f = this.dialog.get_field(d);
f.df.reqd = !for_variant;
f.refresh();
});
}
init_for_item_template_trigger() {
var me = this;
me.dialog.fields_dict["item_template"].df.onchange = () => {
var template = me.dialog.fields_dict.item_template.input.value;
me.template_doc = null;
if (template) {
frappe.call({
method: "frappe.client.get",
args: {
doctype: "Item",
name: template
},
callback: function(r) {
me.template_doc = r.message;
me.is_manufacturer = false;
if (me.template_doc.variant_based_on === "Manufacturer") {
me.init_post_template_trigger_operations(true, [], true);
} else {
me.init_post_template_trigger_operations(false, me.template_doc.attributes, false);
me.render_attributes(me.template_doc.attributes);
}
}
});
} else {
me.dialog.get_field('attribute_html').toggle(false);
me.init_post_template_trigger_operations(false, [], true);
}
}
}
init_post_template_trigger_operations(is_manufacturer, attributes, attributes_flag) {
this.attributes = attributes;
this.attribute_values = {};
this.attributes_count = attributes.length;
this.dialog.fields_dict.attribute_html.$wrapper.find(".attributes").empty();
this.is_manufacturer = is_manufacturer;
this.toggle_manufacturer_fields();
this.dialog.fields_dict.attribute_html.$wrapper.find(".attributes").toggleClass("hide-control", attributes_flag);
this.dialog.fields_dict.attribute_html.$wrapper.find(".attributes-header").toggleClass("hide-control", attributes_flag);
}
toggle_manufacturer_fields() {
var me = this;
$.each(this.manufacturer_fields, function(i, dialog_field) {
me.dialog.get_field(dialog_field.fieldname).df.hidden = !me.is_manufacturer;
me.dialog.get_field(dialog_field.fieldname).df.reqd = dialog_field.fieldname == 'manufacturer' ? me.is_manufacturer : false;
me.dialog.get_field(dialog_field.fieldname).refresh();
});
}
initiate_render_attributes() {
this.dialog.fields_dict.attribute_html.$wrapper.find(".attributes").empty();
this.render_attributes(this.attributes);
}
render_attributes(attributes) {
var me = this;
this.dialog.get_field('attribute_html').toggle(true);
$.each(attributes, function(index, row) {
var desc = "";
var fieldtype = "Data";
if (row.numeric_values) {
fieldtype = "Float";
desc = "Min Value: " + row.from_range + " , Max Value: " + row.to_range + ", in Increments of: " + row.increment;
}
me.init_make_control(fieldtype, row);
me[row.attribute].set_value(me.attribute_values[row.attribute] || "");
me[row.attribute].$wrapper.toggleClass("has-error", me.attribute_values[row.attribute] ? false : true);
// Set Label explicitly as make_control is not displaying label
$(me[row.attribute].label_area).text(__(row.attribute));
if (desc) {
$(repl(`<p class="help-box small text-muted hidden-xs">%(desc)s</p>`, {
"desc": desc
})).insertAfter(me[row.attribute].input_area);
}
if (!row.numeric_values) {
me.init_awesomplete_for_attribute(row);
} else {
me[row.attribute].$input.on("change", function() {
me.attribute_values[row.attribute] = $(this).val();
$(this).closest(".frappe-control").toggleClass("has-error", $(this).val() ? false : true);
});
}
});
}
init_make_control(fieldtype, row) {
this[row.attribute] = frappe.ui.form.make_control({
df: {
"fieldtype": fieldtype,
"label": row.attribute,
"fieldname": row.attribute,
"options": row.options || ""
},
parent: $(this.dialog.fields_dict.attribute_html.wrapper).find(".attributes"),
only_input: false
});
this[row.attribute].make_input();
}
init_awesomplete_for_attribute(row) {
var me = this;
this[row.attribute].input.awesomplete = new Awesomplete(this[row.attribute].input, {
minChars: 0,
maxItems: 99,
autoFirst: true,
list: [],
});
this[row.attribute].$input.on('input', function(e) {
frappe.call({
method: "frappe.client.get_list",
args: {
doctype: "Item Attribute Value",
filters: [
["parent", "=", $(e.target).attr("data-fieldname")],
["attribute_value", "like", e.target.value + "%"]
],
fields: ["attribute_value"],
parent: "Item Attribute"
},
callback: function(r) {
if (r.message) {
e.target.awesomplete.list = r.message.map(function(d) {
return d.attribute_value;
});
}
}
});
}).on('focus', function(e) {
$(e.target).val('').trigger('input');
}).on("awesomplete-close", function (e) {
me.attribute_values[$(e.target).attr("data-fieldname")] = e.target.value;
$(e.target).closest(".frappe-control").toggleClass("has-error", e.target.value ? false : true);
});
}
get_variant_doc() {
var me = this;
var variant_doc = {};
var attribute = this.validate_mandatory_attributes();
if (Object.keys(attribute).length) {
frappe.call({
method: "erpnext.controllers.item_variant.create_variant_doc_for_quick_entry",
args: {
"template": me.dialog.fields_dict.item_template.$input.val(),
args: attribute
},
async: false,
callback: function(r) {
if (Object.prototype.toString.call(r.message) == "[object Object]") {
variant_doc = r.message;
} else {
var msgprint_dialog = frappe.msgprint(__("Item Variant {0} already exists with same attributes", [repl('<a class="strong variant-click" data-item-code="%(item)s" \
>%(item)s</a>', {
item: r.message
})]));
msgprint_dialog.$wrapper.find(".variant-click").on("click", function() {
msgprint_dialog.hide();
me.dialog.hide();
if (frappe._from_link) {
frappe._from_link.set_value($(this).attr("data-item-code"));
} else {
frappe.set_route('Form', "Item", $(this).attr("data-item-code"));
}
});
}
}
})
}
return variant_doc;
}
validate_mandatory_attributes() {
var me = this;
var attribute = {};
var mandatory = [];
$.each(this.attributes, function(index, attr) {
var value = me.attribute_values[attr.attribute] || "";
if (value) {
attribute[attr.attribute] = attr.numeric_values ? flt(value) : value;
} else {
mandatory.push(attr.attribute);
}
})
if (this.is_manufacturer) {
$.each(this.manufacturer_fields, function(index, field) {
attribute[field.fieldname] = me.dialog.fields_dict[field.fieldname].input.value;
});
}
return attribute;
}
};

View File

@ -2,12 +2,14 @@
"charts": [],
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Quality Goal\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Quality Procedure\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Quality Inspection\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Quality Review\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Quality Action\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Non Conformance\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Goal and Procedure\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Feedback\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Meeting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Review and Action\",\"col\":4}}]",
"creation": "2020-03-02 15:49:28.632014",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "quality",
"idx": 0,
"is_hidden": 0,
"label": "Quality",
"links": [
{
@ -142,16 +144,18 @@
"type": "Link"
}
],
"modified": "2022-01-13 17:42:20.105187",
"modified": "2023-05-24 14:47:22.597974",
"modified_by": "Administrator",
"module": "Quality Management",
"name": "Quality",
"number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
"sequence_id": 21.0,
"sequence_id": 9.0,
"shortcuts": [
{
"color": "Grey",

View File

@ -288,7 +288,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
)
# sales team
for d in customer.get("sales_team", []):
for d in customer.get("sales_team") or []:
target.append(
"sales_team",
{

View File

@ -47,21 +47,50 @@ frappe.ui.form.on("Sales Order", {
frm.set_df_property('packed_items', 'cannot_add_rows', true);
frm.set_df_property('packed_items', 'cannot_delete_rows', true);
},
refresh: function(frm) {
if(frm.doc.docstatus === 1 && frm.doc.status !== 'Closed'
&& flt(frm.doc.per_delivered, 6) < 100 && flt(frm.doc.per_billed, 6) < 100) {
frm.add_custom_button(__('Update Items'), () => {
erpnext.utils.update_child_items({
frm: frm,
child_docname: "items",
child_doctype: "Sales Order Detail",
cannot_add_row: false,
})
});
if(frm.doc.docstatus === 1) {
if (frm.doc.status !== 'Closed' && flt(frm.doc.per_delivered, 6) < 100 && flt(frm.doc.per_billed, 6) < 100) {
frm.add_custom_button(__('Update Items'), () => {
erpnext.utils.update_child_items({
frm: frm,
child_docname: "items",
child_doctype: "Sales Order Detail",
cannot_add_row: false,
})
});
// Stock Reservation > Reserve button will be only visible if the SO has unreserved stock.
if (frm.doc.__onload && frm.doc.__onload.has_unreserved_stock) {
frm.add_custom_button(__('Reserve'), () => frm.events.create_stock_reservation_entries(frm), __('Stock Reservation'));
}
}
// Stock Reservation > Unreserve button will be only visible if the SO has reserved stock.
if (frm.doc.__onload && frm.doc.__onload.has_reserved_stock) {
frm.add_custom_button(__('Unreserve'), () => frm.events.cancel_stock_reservation_entries(frm), __('Stock Reservation'));
}
}
if (frm.doc.docstatus === 0 && frm.doc.is_internal_customer) {
frm.events.get_items_from_internal_purchase_order(frm);
if (frm.doc.docstatus === 0) {
if (frm.doc.is_internal_customer) {
frm.events.get_items_from_internal_purchase_order(frm);
}
if (frm.is_new()) {
frappe.db.get_single_value("Stock Settings", "enable_stock_reservation").then((value) => {
if (value) {
frappe.db.get_single_value("Stock Settings", "reserve_stock_on_sales_order_submission").then((value) => {
// If `Reserve Stock on Sales Order Submission` is enabled in Stock Settings, set Reserve Stock to 1 else 0.
frm.set_value("reserve_stock", value ? 1 : 0);
})
} else {
// If `Stock Reservation` is disabled in Stock Settings, set Reserve Stock to 0 and read only.
frm.set_value("reserve_stock", 0);
frm.set_df_property("reserve_stock", "read_only", 1);
}
})
}
}
},
@ -137,6 +166,108 @@ frappe.ui.form.on("Sales Order", {
if(!d.delivery_date) d.delivery_date = frm.doc.delivery_date;
});
refresh_field("items");
},
create_stock_reservation_entries(frm) {
let items_data = [];
const dialog = frappe.prompt({fieldname: 'items', fieldtype: 'Table', label: __('Items to Reserve'),
fields: [
{
fieldtype: 'Data',
fieldname: 'name',
label: __('Name'),
reqd: 1,
read_only: 1,
},
{
fieldtype: 'Link',
fieldname: 'item_code',
label: __('Item Code'),
options: 'Item',
reqd: 1,
read_only: 1,
in_list_view: 1,
},
{
fieldtype: 'Link',
fieldname: 'warehouse',
label: __('Warehouse'),
options: 'Warehouse',
reqd: 1,
in_list_view: 1,
get_query: function () {
return {
filters: [
["Warehouse", "is_group", "!=", 1]
]
};
},
},
{
fieldtype: 'Float',
fieldname: 'qty_to_reserve',
label: __('Qty'),
reqd: 1,
in_list_view: 1
}
],
data: items_data,
in_place_edit: true,
get_data: function() {
return items_data;
}
}, function(data) {
if (data.items.length > 0) {
frappe.call({
doc: frm.doc,
method: 'create_stock_reservation_entries',
args: {
items_details: data.items,
notify: true
},
freeze: true,
freeze_message: __('Reserving Stock...'),
callback: (r) => {
frm.doc.__onload.has_unreserved_stock = false;
frm.reload_doc();
}
});
}
}, __("Stock Reservation"), __("Reserve Stock"));
frm.doc.items.forEach(item => {
if (item.reserve_stock) {
let unreserved_qty = (flt(item.stock_qty) - (flt(item.delivered_qty) * flt(item.conversion_factor)) - flt(item.stock_reserved_qty))
if (unreserved_qty > 0) {
dialog.fields_dict.items.df.data.push({
'name': item.name,
'item_code': item.item_code,
'warehouse': item.warehouse,
'qty_to_reserve': (unreserved_qty / flt(item.conversion_factor))
});
}
}
});
dialog.fields_dict.items.grid.refresh();
},
cancel_stock_reservation_entries(frm) {
frappe.call({
method: 'erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry.cancel_stock_reservation_entries',
args: {
voucher_type: frm.doctype,
voucher_no: frm.docname
},
freeze: true,
freeze_message: __('Unreserving Stock...'),
callback: (r) => {
frm.doc.__onload.has_reserved_stock = false;
frm.reload_doc();
}
})
}
});

View File

@ -42,6 +42,7 @@
"scan_barcode",
"column_break_28",
"set_warehouse",
"reserve_stock",
"items_section",
"items",
"section_break_31",
@ -1625,13 +1626,24 @@
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
},
{
"default": "0",
"depends_on": "eval: (doc.docstatus == 0 || doc.reserve_stock)",
"description": "If checked, Stock Reservation Entries will be created on <b>Submit</b>",
"fieldname": "reserve_stock",
"fieldtype": "Check",
"label": "Reserve Stock",
"no_copy": 1,
"print_hide": 1,
"report_hide": 1
}
],
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2023-04-20 11:14:01.036202",
"modified": "2023-04-22 09:55:37.008190",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",
@ -1664,7 +1676,6 @@
"read": 1,
"report": 1,
"role": "Sales Manager",
"set_user_permissions": 1,
"share": 1,
"submit": 1,
"write": 1

View File

@ -30,6 +30,11 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import (
from erpnext.selling.doctype.customer.customer import check_credit_limit
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock.doctype.item.item import get_item_defaults
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
cancel_stock_reservation_entries,
get_sre_reserved_qty_details_for_voucher,
has_reserved_stock,
)
from erpnext.stock.get_item_details import get_default_bom, get_price_list_rate
from erpnext.stock.stock_balance import get_reserved_qty, update_bin_qty
@ -44,6 +49,14 @@ class SalesOrder(SellingController):
def __init__(self, *args, **kwargs):
super(SalesOrder, self).__init__(*args, **kwargs)
def onload(self) -> None:
if frappe.get_cached_value("Stock Settings", None, "enable_stock_reservation"):
if self.has_unreserved_stock():
self.set_onload("has_unreserved_stock", True)
if has_reserved_stock(self.doctype, self.name):
self.set_onload("has_reserved_stock", True)
def validate(self):
super(SalesOrder, self).validate()
self.validate_delivery_date()
@ -241,6 +254,9 @@ class SalesOrder(SellingController):
update_coupon_code_count(self.coupon_code, "used")
if self.get("reserve_stock"):
self.create_stock_reservation_entries()
def on_cancel(self):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
super(SalesOrder, self).on_cancel()
@ -257,6 +273,7 @@ class SalesOrder(SellingController):
self.db_set("status", "Cancelled")
self.update_blanket_order()
cancel_stock_reservation_entries("Sales Order", self.name)
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_order_reference)
if self.coupon_code:
@ -485,6 +502,166 @@ class SalesOrder(SellingController):
).format(item.item_code)
)
def has_unreserved_stock(self) -> bool:
"""Returns True if there is any unreserved item in the Sales Order."""
reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", self.name)
for item in self.get("items"):
if not item.get("reserve_stock"):
continue
unreserved_qty = get_unreserved_qty(item, reserved_qty_details)
if unreserved_qty > 0:
return True
return False
@frappe.whitelist()
def create_stock_reservation_entries(self, items_details=None, notify=True):
"""Creates Stock Reservation Entries for Sales Order Items."""
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
get_available_qty_to_reserve,
validate_stock_reservation_settings,
)
validate_stock_reservation_settings(self)
allow_partial_reservation = frappe.db.get_single_value(
"Stock Settings", "allow_partial_reservation"
)
items = []
if items_details:
for item in items_details:
so_item = frappe.get_doc("Sales Order Item", item["name"])
so_item.reserve_stock = 1
so_item.warehouse = item["warehouse"]
so_item.qty_to_reserve = flt(item["qty_to_reserve"]) * flt(so_item.conversion_factor)
items.append(so_item)
sre_count = 0
reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", self.name)
for item in items or self.get("items"):
# Skip if `Reserved Stock` is not checked for the item.
if not item.get("reserve_stock"):
continue
# Skip if Non-Stock Item.
if not frappe.get_cached_value("Item", item.item_code, "is_stock_item"):
frappe.msgprint(
_("Row #{0}: Stock cannot be reserved for a non-stock Item {1}").format(
item.idx, frappe.bold(item.item_code)
),
title=_("Stock Reservation"),
indicator="yellow",
)
item.db_set("reserve_stock", 0)
continue
# Skip if Group Warehouse.
if frappe.get_cached_value("Warehouse", item.warehouse, "is_group"):
frappe.msgprint(
_("Row #{0}: Stock cannot be reserved in group warehouse {1}.").format(
item.idx, frappe.bold(item.warehouse)
),
title=_("Stock Reservation"),
indicator="yellow",
)
continue
unreserved_qty = get_unreserved_qty(item, reserved_qty_details)
# Stock is already reserved for the item, notify the user and skip the item.
if unreserved_qty <= 0:
frappe.msgprint(
_("Row #{0}: Stock is already reserved for the Item {1}.").format(
item.idx, frappe.bold(item.item_code)
),
title=_("Stock Reservation"),
indicator="yellow",
)
continue
available_qty_to_reserve = get_available_qty_to_reserve(item.item_code, item.warehouse)
# No stock available to reserve, notify the user and skip the item.
if available_qty_to_reserve <= 0:
frappe.msgprint(
_("Row #{0}: No available stock to reserve for the Item {1} in Warehouse {2}.").format(
item.idx, frappe.bold(item.item_code), frappe.bold(item.warehouse)
),
title=_("Stock Reservation"),
indicator="orange",
)
continue
# The quantity which can be reserved.
qty_to_be_reserved = min(unreserved_qty, available_qty_to_reserve)
if hasattr(item, "qty_to_reserve"):
if item.qty_to_reserve <= 0:
frappe.msgprint(
_("Row #{0}: Quantity to reserve for the Item {1} should be greater than 0.").format(
item.idx, frappe.bold(item.item_code)
),
title=_("Stock Reservation"),
indicator="orange",
)
continue
else:
qty_to_be_reserved = min(qty_to_be_reserved, item.qty_to_reserve)
# Partial Reservation
if qty_to_be_reserved < unreserved_qty:
if not item.get("qty_to_reserve") or qty_to_be_reserved < flt(item.get("qty_to_reserve")):
frappe.msgprint(
_("Row #{0}: Only {1} available to reserve for the Item {2}").format(
item.idx,
frappe.bold(str(qty_to_be_reserved / item.conversion_factor) + " " + item.uom),
frappe.bold(item.item_code),
),
title=_("Stock Reservation"),
indicator="orange",
)
# Skip the item if `Partial Reservation` is disabled in the Stock Settings.
if not allow_partial_reservation:
continue
# Create and Submit Stock Reservation Entry
sre = frappe.new_doc("Stock Reservation Entry")
sre.item_code = item.item_code
sre.warehouse = item.warehouse
sre.voucher_type = self.doctype
sre.voucher_no = self.name
sre.voucher_detail_no = item.name
sre.available_qty = available_qty_to_reserve
sre.voucher_qty = item.stock_qty
sre.reserved_qty = qty_to_be_reserved
sre.company = self.company
sre.stock_uom = item.stock_uom
sre.project = self.project
sre.save()
sre.submit()
sre_count += 1
if sre_count and notify:
frappe.msgprint(_("Stock Reservation Entries Created"), alert=True, indicator="green")
def get_unreserved_qty(item: object, reserved_qty_details: dict) -> float:
"""Returns the unreserved quantity for the Sales Order Item."""
existing_reserved_qty = reserved_qty_details.get(item.name, 0)
return (
item.stock_qty
- flt(item.delivered_qty) * item.get("conversion_factor", 1)
- existing_reserved_qty
)
def get_list_context(context=None):
from erpnext.controllers.website_list_for_contact import get_list_context
@ -680,7 +857,6 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False):
}
target_doc = get_mapped_doc("Sales Order", source_name, mapper, target_doc, set_missing_values)
target_doc.set_onload("ignore_price_list", True)
return target_doc

View File

@ -11,6 +11,7 @@ def get_data():
"Payment Request": "reference_name",
"Auto Repeat": "reference_document",
"Maintenance Visit": "prevdoc_docname",
"Stock Reservation Entry": "voucher_no",
},
"internal_links": {
"Quotation": ["items", "prevdoc_docname"],
@ -23,7 +24,7 @@ def get_data():
{"label": _("Purchasing"), "items": ["Material Request", "Purchase Order"]},
{"label": _("Projects"), "items": ["Project"]},
{"label": _("Manufacturing"), "items": ["Work Order"]},
{"label": _("Reference"), "items": ["Quotation", "Auto Repeat"]},
{"label": _("Reference"), "items": ["Quotation", "Auto Repeat", "Stock Reservation Entry"]},
{"label": _("Payment"), "items": ["Payment Entry", "Payment Request", "Journal Entry"]},
],
}

View File

@ -1878,6 +1878,141 @@ class TestSalesOrder(FrappeTestCase):
self.assertEqual(pe.references[1].reference_name, so.name)
self.assertEqual(pe.references[1].allocated_amount, 300)
@change_settings("Stock Settings", {"enable_stock_reservation": 1})
def test_stock_reservation_against_sales_order(self) -> None:
from random import randint, uniform
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
cancel_stock_reservation_entries,
get_sre_reserved_qty_details_for_voucher,
get_stock_reservation_entries_for_voucher,
has_reserved_stock,
)
from erpnext.stock.doctype.stock_reservation_entry.test_stock_reservation_entry import (
create_items,
create_material_receipt,
)
items_details, warehouse = create_items(), "_Test Warehouse - _TC"
se = create_material_receipt(items_details, warehouse, qty=10)
item_list = []
for item_code, properties in items_details.items():
stock_uom = properties.stock_uom
item_list.append(
{
"item_code": item_code,
"warehouse": warehouse,
"qty": flt(uniform(11, 100), 0 if stock_uom == "Nos" else 3),
"uom": stock_uom,
"rate": randint(10, 200),
}
)
so = make_sales_order(
item_list=item_list,
warehouse="_Test Warehouse - _TC",
)
# Test - 1: Stock should not be reserved if the Available Qty to Reserve is less than the Ordered Qty and Partial Reservation is disabled in Stock Settings.
with change_settings("Stock Settings", {"allow_partial_reservation": 0}):
so.create_stock_reservation_entries()
self.assertFalse(has_reserved_stock("Sales Order", so.name))
# Test - 2: Stock should be Partially Reserved if the Partial Reservation is enabled in Stock Settings.
with change_settings("Stock Settings", {"allow_partial_reservation": 1}):
so.create_stock_reservation_entries()
so.load_from_db()
self.assertTrue(has_reserved_stock("Sales Order", so.name))
for item in so.items:
sre_details = get_stock_reservation_entries_for_voucher(
"Sales Order", so.name, item.name, fields=["reserved_qty", "status"]
)
self.assertEqual(item.stock_reserved_qty, sre_details[0].reserved_qty)
self.assertEqual(sre_details[0].status, "Partially Reserved")
se.cancel()
# Test - 3: Stock should be fully Reserved if the Available Qty to Reserve is greater than the Un-reserved Qty.
create_material_receipt(items_details, warehouse, qty=110)
so.create_stock_reservation_entries()
so.load_from_db()
reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", so.name)
for item in so.items:
reserved_qty = reserved_qty_details[item.name]
self.assertEqual(item.stock_reserved_qty, reserved_qty)
self.assertEqual(item.stock_qty, item.stock_reserved_qty)
# Test - 4: Stock should get unreserved on cancellation of Stock Reservation Entries.
cancel_stock_reservation_entries("Sales Order", so.name)
so.load_from_db()
self.assertFalse(has_reserved_stock("Sales Order", so.name))
for item in so.items:
self.assertEqual(item.stock_reserved_qty, 0)
# Test - 5: Re-reserve the stock.
so.create_stock_reservation_entries()
self.assertTrue(has_reserved_stock("Sales Order", so.name))
# Test - 6: Stock should get unreserved on cancellation of Sales Order.
so.cancel()
so.load_from_db()
self.assertFalse(has_reserved_stock("Sales Order", so.name))
for item in so.items:
self.assertEqual(item.stock_reserved_qty, 0)
# Create Sales Order and Reserve Stock.
so = make_sales_order(
item_list=item_list,
warehouse="_Test Warehouse - _TC",
)
so.create_stock_reservation_entries()
# Test - 7: Partial Delivery against Sales Order.
dn1 = make_delivery_note(so.name)
for item in dn1.items:
item.qty = flt(uniform(1, 10), 0 if item.stock_uom == "Nos" else 3)
dn1.save()
dn1.submit()
for item in so.items:
sre_details = get_stock_reservation_entries_for_voucher(
"Sales Order", so.name, item.name, fields=["delivered_qty", "status"]
)
self.assertGreater(sre_details[0].delivered_qty, 0)
self.assertEqual(sre_details[0].status, "Partially Delivered")
# Test - 8: Over Delivery against Sales Order, SRE Delivered Qty should not be greater than the SRE Reserved Qty.
with change_settings("Stock Settings", {"over_delivery_receipt_allowance": 100}):
dn2 = make_delivery_note(so.name)
for item in dn2.items:
item.qty += flt(uniform(1, 10), 0 if item.stock_uom == "Nos" else 3)
dn2.save()
dn2.submit()
for item in so.items:
sre_details = frappe.db.get_all(
"Stock Reservation Entry",
filters={
"voucher_type": "Sales Order",
"voucher_no": so.name,
"voucher_detail_no": item.name,
},
fields=["status", "reserved_qty", "delivered_qty"],
)
for sre_detail in sre_details:
self.assertEqual(sre_detail.reserved_qty, sre_detail.delivered_qty)
self.assertEqual(sre_detail.status, "Delivered")
def test_delivered_item_material_request(self):
"SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO."
from erpnext.manufacturing.doctype.work_order.work_order import (

View File

@ -10,6 +10,7 @@
"item_code",
"customer_item_code",
"ensure_delivery_based_on_produced_serial_no",
"reserve_stock",
"col_break1",
"delivery_date",
"item_name",
@ -27,6 +28,7 @@
"uom",
"conversion_factor",
"stock_qty",
"stock_reserved_qty",
"section_break_16",
"price_list_rate",
"base_price_list_rate",
@ -859,12 +861,33 @@
"fieldname": "material_request_item",
"fieldtype": "Data",
"label": "Material Request Item"
},
{
"allow_on_submit": 1,
"default": "1",
"fieldname": "reserve_stock",
"fieldtype": "Check",
"label": "Reserve Stock",
"print_hide": 1,
"report_hide": 1
},
{
"default": "0",
"depends_on": "eval: doc.stock_reserved_qty",
"fieldname": "stock_reserved_qty",
"fieldtype": "Float",
"label": "Stock Reserved Qty (in Stock UOM)",
"no_copy": 1,
"non_negative": 1,
"print_hide": 1,
"read_only": 1,
"report_hide": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2022-12-25 02:51:10.247569",
"modified": "2023-04-04 10:44:05.707488",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",

View File

@ -2,9 +2,11 @@
"creation": "2021-11-23 12:00:36.138824",
"docstatus": 0,
"doctype": "Form Tour",
"first_document": 0,
"idx": 0,
"include_name_field": 0,
"is_standard": 1,
"modified": "2021-11-23 12:02:48.010298",
"modified": "2023-05-23 12:51:48.684517",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation",
@ -14,51 +16,43 @@
"steps": [
{
"description": "Select a customer or lead for whom this quotation is being prepared. Let's select a Customer.",
"field": "",
"fieldname": "quotation_to",
"fieldtype": "Link",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Quotation To",
"parent_field": "",
"position": "Right",
"title": "Quotation To"
},
{
"description": "Select a specific Customer to whom this quotation will be sent.",
"field": "",
"fieldname": "party_name",
"fieldtype": "Dynamic Link",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Party",
"parent_field": "",
"position": "Right",
"title": "Party"
},
{
"child_doctype": "Quotation Item",
"description": "Select an item for which you will be quoting a price.",
"field": "",
"fieldname": "items",
"fieldtype": "Table",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Items",
"parent_field": "",
"parent_fieldname": "items",
"position": "Bottom",
"title": "Items"
},
{
"description": "You can select pre-populated Sales Taxes and Charges from here.",
"field": "",
"fieldname": "taxes",
"fieldtype": "Table",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Sales Taxes and Charges",
"parent_field": "",
"position": "Bottom",
"title": "Sales Taxes and Charges"
}

View File

@ -1,123 +0,0 @@
{
"charts": [],
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Point Of Sale\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings & Configurations\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Loyalty Program\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Opening & Closing\",\"col\":4}}]",
"creation": "2020-03-02 17:18:32.505616",
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "retail",
"idx": 0,
"label": "Retail",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Settings & Configurations",
"link_count": 2,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Point-of-Sale Profile",
"link_count": 0,
"link_to": "POS Profile",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "POS Settings",
"link_count": 0,
"link_to": "POS Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Loyalty Program",
"link_count": 2,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loyalty Program",
"link_count": 0,
"link_to": "Loyalty Program",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loyalty Point Entry",
"link_count": 0,
"link_to": "Loyalty Point Entry",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Opening & Closing",
"link_count": 2,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "POS Opening Entry",
"link_count": 0,
"link_to": "POS Opening Entry",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "POS Closing Entry",
"link_count": 0,
"link_to": "POS Closing Entry",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
"modified": "2022-01-13 18:07:56.711095",
"modified_by": "Administrator",
"module": "Selling",
"name": "Retail",
"owner": "Administrator",
"parent_page": "",
"public": 1,
"restrict_to_domain": "Retail",
"roles": [],
"sequence_id": 22.0,
"shortcuts": [
{
"doc_view": "",
"label": "Point Of Sale",
"link_to": "point-of-sale",
"type": "Page"
}
],
"title": "Retail"
}

View File

@ -5,14 +5,16 @@
"label": "Sales Order Trends"
}
],
"content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Selling\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Sales Order Trends\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Quick Access</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Order\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Analytics\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Order Analysis\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Selling\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Items and Pricing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]",
"content": "[{\"id\":\"ow595dYDrI\",\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Selling\",\"col\":12}},{\"id\":\"vBSf8Vi9U8\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Sales Order Trends\",\"col\":12}},{\"id\":\"aW2i5R5GRP\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"1it3dCOnm6\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Quick Access</b></span>\",\"col\":12}},{\"id\":\"x7pLl-spS4\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"id\":\"SSGrXWmY-H\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Order\",\"col\":3}},{\"id\":\"-5J_yLxDaS\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Analytics\",\"col\":3}},{\"id\":\"6YEYpnIBKV\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Point of Sale\",\"col\":3}},{\"id\":\"c_GjZuZ2oN\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"oNjjNbnUHp\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"0BcePLg0g1\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"uze5dJ1ipL\",\"type\":\"card\",\"data\":{\"card_name\":\"Selling\",\"col\":4}},{\"id\":\"3j2fYwMAkq\",\"type\":\"card\",\"data\":{\"card_name\":\"Point of Sale\",\"col\":4}},{\"id\":\"xImm8NepFt\",\"type\":\"card\",\"data\":{\"card_name\":\"Items and Pricing\",\"col\":4}},{\"id\":\"6MjIe7KCQo\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"id\":\"lBu2EKgmJF\",\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"id\":\"1ARHrjg4kI\",\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]",
"creation": "2020-01-28 11:49:12.092882",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "sell",
"idx": 0,
"is_hidden": 0,
"label": "Selling",
"links": [
{
@ -317,140 +319,68 @@
{
"hidden": 0,
"is_query_report": 0,
"label": "Other Reports",
"link_count": 12,
"label": "Point of Sale",
"link_count": 6,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Lead",
"hidden": 0,
"is_query_report": 1,
"label": "Lead Details",
"is_query_report": 0,
"label": "Point-of-Sale Profile",
"link_count": 0,
"link_to": "Lead Details",
"link_type": "Report",
"link_to": "POS Profile",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Address",
"hidden": 0,
"is_query_report": 1,
"label": "Customer Addresses And Contacts",
"is_query_report": 0,
"label": "POS Settings",
"link_count": 0,
"link_to": "Address And Contacts",
"link_type": "Report",
"link_to": "POS Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Item",
"hidden": 0,
"is_query_report": 1,
"label": "Available Stock for Packing Items",
"is_query_report": 0,
"label": "POS Opening Entry",
"link_count": 0,
"link_to": "Available Stock for Packing Items",
"link_type": "Report",
"link_to": "POS Opening Entry",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Sales Order",
"hidden": 0,
"is_query_report": 1,
"label": "Pending SO Items For Purchase Request",
"is_query_report": 0,
"label": "POS Closing Entry",
"link_count": 0,
"link_to": "Pending SO Items For Purchase Request",
"link_type": "Report",
"link_to": "POS Closing Entry",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Delivery Note",
"hidden": 0,
"is_query_report": 1,
"label": "Delivery Note Trends",
"is_query_report": 0,
"label": "Loyalty Program",
"link_count": 0,
"link_to": "Delivery Note Trends",
"link_type": "Report",
"link_to": "Loyalty Program",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Sales Invoice",
"hidden": 0,
"is_query_report": 1,
"label": "Sales Invoice Trends",
"is_query_report": 0,
"label": "Loyalty Point Entry",
"link_count": 0,
"link_to": "Sales Invoice Trends",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Customer",
"hidden": 0,
"is_query_report": 1,
"label": "Customer Credit Balance",
"link_count": 0,
"link_to": "Customer Credit Balance",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Customer",
"hidden": 0,
"is_query_report": 1,
"label": "Customers Without Any Sales Transactions",
"link_count": 0,
"link_to": "Customers Without Any Sales Transactions",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Customer",
"hidden": 0,
"is_query_report": 1,
"label": "Sales Partners Commission",
"link_count": 0,
"link_to": "Sales Partners Commission",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Sales Order",
"hidden": 0,
"is_query_report": 1,
"label": "Territory Target Variance Based On Item Group",
"link_count": 0,
"link_to": "Territory Target Variance Based On Item Group",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Sales Order",
"hidden": 0,
"is_query_report": 1,
"label": "Sales Person Target Variance Based On Item Group",
"link_count": 0,
"link_to": "Sales Person Target Variance Based On Item Group",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Sales Order",
"hidden": 0,
"is_query_report": 1,
"label": "Sales Partner Target Variance Based On Item Group",
"link_count": 0,
"link_to": "Sales Partner Target Variance based on Item Group",
"link_type": "Report",
"link_to": "Loyalty Point Entry",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
@ -458,7 +388,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Key Reports",
"link_count": 22,
"link_count": 9,
"onboard": 0,
"type": "Card Break"
},
@ -562,15 +492,12 @@
"type": "Link"
},
{
"dependencies": "Lead",
"hidden": 0,
"is_query_report": 1,
"label": "Lead Details",
"link_count": 0,
"link_to": "Lead Details",
"link_type": "Report",
"is_query_report": 0,
"label": "Other Reports",
"link_count": 11,
"onboard": 0,
"type": "Link"
"type": "Card Break"
},
{
"dependencies": "Address",
@ -692,29 +619,26 @@
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 1,
"label": "Payment Terms Status for Sales Order",
"link_count": 0,
"link_to": "Payment Terms Status for Sales Order",
"link_type": "Report",
"onboard": 0,
"type": "Link"
}
],
"modified": "2023-04-16 13:29:55.087240",
"modified": "2023-05-26 16:31:53.634851",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling",
"number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
"sequence_id": 23.0,
"sequence_id": 6.0,
"shortcuts": [
{
"label": "Point of Sale",
"link_to": "point-of-sale",
"type": "Page"
},
{
"color": "Grey",
"format": "{} Available",
@ -739,11 +663,6 @@
"stats_filter": "{ \"Status\": \"Open\" }",
"type": "Report"
},
{
"label": "Sales Order Analysis",
"link_to": "Sales Order Analysis",
"type": "Report"
},
{
"label": "Dashboard",
"link_to": "Selling",

View File

@ -31,6 +31,7 @@ def after_install():
add_standard_navbar_items()
add_app_name()
setup_log_settings()
hide_workspaces()
frappe.db.commit()
@ -205,3 +206,8 @@ def setup_log_settings():
log_settings.append("logs_to_clear", {"ref_doctype": "Repost Item Valuation", "days": 60})
log_settings.save(ignore_permissions=True)
def hide_workspaces():
for ws in ["Integration", "Settings"]:
frappe.db.set_value("Workspace", ws, "public", 0)

View File

@ -25,15 +25,12 @@
"documentation_url": "https://docs.erpnext.com/docs/v14/user/manual/en/setting-up/company-setup",
"idx": 0,
"is_complete": 0,
"modified": "2023-05-16 13:13:24.043792",
"modified": "2023-05-23 13:20:19.703506",
"modified_by": "Administrator",
"module": "Setup",
"name": "Home",
"owner": "Administrator",
"steps": [
{
"step": "Navigation Help"
},
{
"step": "Create an Item"
},
@ -41,13 +38,10 @@
"step": "Create a Customer"
},
{
"step": "Create a Supplier"
},
{
"step": "Create a Quotation"
"step": "Create Your First Sales Invoice"
}
],
"subtitle": "Item, Customer, Supplier, Navigation Help and Quotation",
"subtitle": "Item, Customer, Supplier and Quotation",
"success_message": "You're ready to start your journey with ERPNext",
"title": "Let's begin your journey with ERPNext"
}

View File

@ -9,7 +9,7 @@
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2023-05-16 12:54:54.112364",
"modified": "2023-05-23 12:45:55.138580",
"modified_by": "Administrator",
"name": "Create a Customer",
"owner": "Administrator",

View File

@ -9,7 +9,7 @@
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2023-05-16 12:55:08.610113",
"modified": "2023-05-19 15:32:55.069257",
"modified_by": "Administrator",
"name": "Create a Supplier",
"owner": "Administrator",

View File

@ -11,7 +11,7 @@
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2023-05-16 12:56:40.355878",
"modified": "2023-05-23 12:43:08.484206",
"modified_by": "Administrator",
"name": "Create an Item",
"owner": "Administrator",

View File

@ -0,0 +1,20 @@
{
"action": "Create Entry",
"creation": "2020-05-14 17:48:21.019019",
"description": "# All about sales invoice\n\nA Sales Invoice is a bill that you send to your Customers against which the Customer makes the payment. Sales Invoice is an accounting transaction. On submission of Sales Invoice, the system updates the receivable and books income against a Customer Account.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2023-05-22 21:20:15.589644",
"modified_by": "Administrator",
"name": "Create Your First Sales Invoice",
"owner": "Administrator",
"reference_document": "Sales Invoice",
"show_form_tour": 1,
"show_full_form": 1,
"title": "Create Your First Sales Invoice ",
"validate_action": 1
}

View File

@ -1,31 +1,467 @@
{
"charts": [],
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Projects Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"HR Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Selling Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Buying Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Support Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Shopping Cart Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Portal Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Domain Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Products Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Naming Series\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Manufacturing Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Education Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Hotel Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"CRM Settings\",\"col\":3}}]",
"content": "[{\"id\":\"NO5yYHJopc\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t</b></span>\",\"col\":12}},{\"id\":\"CDxIM-WuZ9\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"System Settings\",\"col\":3}},{\"id\":\"-Uh7DKJNJX\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Settings\",\"col\":3}},{\"id\":\"K9ST9xcDXh\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Settings\",\"col\":3}},{\"id\":\"27IdVHVQMb\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Selling Settings\",\"col\":3}},{\"id\":\"Rwp5zff88b\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Buying Settings\",\"col\":3}},{\"id\":\"hkfnQ2sevf\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Global Defaults\",\"col\":3}},{\"id\":\"jjxI_PDawD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Print Settings\",\"col\":3}},{\"id\":\"R3CoYYFXye\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yynbm1J_VO\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Settings</b></span>\",\"col\":12}},{\"id\":\"KDCv2MvSg3\",\"type\":\"card\",\"data\":{\"card_name\":\"Module Settings\",\"col\":4}},{\"id\":\"Q0_bqT7cxQ\",\"type\":\"card\",\"data\":{\"card_name\":\"Email / Notifications\",\"col\":4}},{\"id\":\"UnqK5haBnh\",\"type\":\"card\",\"data\":{\"card_name\":\"Website\",\"col\":4}},{\"id\":\"kp7u1H5hCd\",\"type\":\"card\",\"data\":{\"card_name\":\"Core\",\"col\":4}},{\"id\":\"Ufc3jycgy9\",\"type\":\"card\",\"data\":{\"card_name\":\"Printing\",\"col\":4}},{\"id\":\"89bSNzv3Yh\",\"type\":\"card\",\"data\":{\"card_name\":\"Workflow\",\"col\":4}}]",
"creation": "2022-01-27 13:14:47.349433",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "setting",
"idx": 0,
"is_hidden": 0,
"label": "ERPNext Settings",
"links": [],
"modified": "2022-06-27 16:53:07.056620",
"links": [
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Import Data",
"link_count": 0,
"link_to": "Data Import",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Export Data",
"link_count": 0,
"link_to": "Data Export",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Bulk Update",
"link_count": 0,
"link_to": "Bulk Update",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Download Backups",
"link_count": 0,
"link_to": "backups",
"link_type": "Page",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Deleted Documents",
"link_count": 0,
"link_to": "Deleted Document",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Email / Notifications",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Email Account",
"link_count": 0,
"link_to": "Email Account",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Email Domain",
"link_count": 0,
"link_to": "Email Domain",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Notification",
"link_count": 0,
"link_to": "Notification",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Email Template",
"link_count": 0,
"link_to": "Email Template",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Auto Email Report",
"link_count": 0,
"link_to": "Auto Email Report",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Newsletter",
"link_count": 0,
"link_to": "Newsletter",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Notification Settings",
"link_count": 0,
"link_to": "Notification Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Website",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Website Settings",
"link_count": 0,
"link_to": "Website Settings",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Website Theme",
"link_count": 0,
"link_to": "Website Theme",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Website Script",
"link_count": 0,
"link_to": "Website Script",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "About Us Settings",
"link_count": 0,
"link_to": "About Us Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Contact Us Settings",
"link_count": 0,
"link_to": "Contact Us Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Printing",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Print Format Builder",
"link_count": 0,
"link_to": "print-format-builder",
"link_type": "Page",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Print Settings",
"link_count": 0,
"link_to": "Print Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Print Format",
"link_count": 0,
"link_to": "Print Format",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Print Style",
"link_count": 0,
"link_to": "Print Style",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Workflow",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Workflow",
"link_count": 0,
"link_to": "Workflow",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Workflow State",
"link_count": 0,
"link_to": "Workflow State",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Workflow Action",
"link_count": 0,
"link_to": "Workflow Action",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Core",
"link_count": 3,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "System Settings",
"link_count": 0,
"link_to": "System Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Domain Settings",
"link_count": 0,
"link_to": "Domain Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Global Defaults",
"link_count": 0,
"link_to": "Global Defaults",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Module Settings",
"link_count": 8,
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Accounts Settings",
"link_count": 0,
"link_to": "Accounts Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Stock Settings",
"link_count": 0,
"link_to": "Stock Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Selling Settings",
"link_count": 0,
"link_to": "Selling Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Buying Settings",
"link_count": 0,
"link_to": "Buying Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Manufacturing Settings",
"link_count": 0,
"link_to": "Manufacturing Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "CRM Settings",
"link_count": 0,
"link_to": "CRM Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Projects Settings",
"link_count": 0,
"link_to": "Projects Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Support Settings",
"link_count": 0,
"link_to": "Support Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
"modified": "2023-05-24 14:47:25.356531",
"modified_by": "Administrator",
"module": "Setup",
"name": "ERPNext Settings",
"number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
"sequence_id": 12.0,
"sequence_id": 19.0,
"shortcuts": [
{
"icon": "project",
"label": "Projects Settings",
"link_to": "Projects Settings",
"color": "Grey",
"doc_view": "List",
"label": "Print Settings",
"link_to": "Print Settings",
"type": "DocType"
},
{
"color": "Grey",
"doc_view": "List",
"label": "System Settings",
"link_to": "System Settings",
"type": "DocType"
},
{
@ -34,6 +470,13 @@
"link_to": "Accounts Settings",
"type": "DocType"
},
{
"color": "Grey",
"doc_view": "List",
"label": "Global Defaults",
"link_to": "Global Defaults",
"type": "DocType"
},
{
"icon": "stock",
"label": "Stock Settings",
@ -51,44 +494,6 @@
"label": "Buying Settings",
"link_to": "Buying Settings",
"type": "DocType"
},
{
"icon": "support",
"label": "Support Settings",
"link_to": "Support Settings",
"type": "DocType"
},
{
"icon": "retail",
"label": "E Commerce Settings",
"link_to": "E Commerce Settings",
"type": "DocType"
},
{
"icon": "website",
"label": "Portal Settings",
"link_to": "Portal Settings",
"type": "DocType"
},
{
"icon": "organization",
"label": "Manufacturing Settings",
"link_to": "Manufacturing Settings",
"restrict_to_domain": "Manufacturing",
"type": "DocType"
},
{
"icon": "setting",
"label": "Domain Settings",
"link_to": "Domain Settings",
"type": "DocType"
},
{
"doc_view": "",
"icon": "crm",
"label": "CRM Settings",
"link_to": "CRM Settings",
"type": "DocType"
}
],
"title": "ERPNext Settings"

View File

@ -1,13 +1,15 @@
{
"charts": [],
"content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Home\",\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leaderboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Stock\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Human Resources\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"CRM\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data Import and Settings\",\"col\":4}}]",
"content": "[{\"id\":\"aCk49ShVRs\",\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Home\",\"col\":12}},{\"id\":\"kb3XPLg8lb\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"nWd2KJPW8l\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"id\":\"snrzfbFr5Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":3}},{\"id\":\"SHJKakmLLf\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier\",\"col\":3}},{\"id\":\"CPxEyhaf3G\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"id\":\"WU4F-HUcIQ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leaderboard\",\"col\":3}},{\"id\":\"d_KVM1gsf9\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"JVu8-FJZCu\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"JiuSi0ubOg\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounting\",\"col\":4}},{\"id\":\"ji2Jlm3Q8i\",\"type\":\"card\",\"data\":{\"card_name\":\"Stock\",\"col\":4}},{\"id\":\"N61oiXpuwK\",\"type\":\"card\",\"data\":{\"card_name\":\"CRM\",\"col\":4}},{\"id\":\"6J0CVl1mPo\",\"type\":\"card\",\"data\":{\"card_name\":\"Data Import and Settings\",\"col\":4}}]",
"creation": "2020-01-23 13:46:38.833076",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "getting-started",
"idx": 0,
"is_hidden": 0,
"label": "Home",
"links": [
{
@ -230,10 +232,11 @@
"type": "Link"
}
],
"modified": "2022-06-27 16:54:35.462176",
"modified": "2023-05-24 14:47:18.765388",
"modified_by": "Administrator",
"module": "Setup",
"name": "Home",
"number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,

View File

@ -0,0 +1,39 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Closing Stock Balance", {
refresh(frm) {
frm.trigger("generate_closing_balance");
frm.trigger("regenerate_closing_balance");
},
generate_closing_balance(frm) {
if (in_list(["Queued", "Failed"], frm.doc.status)) {
frm.add_custom_button(__("Generate Closing Stock Balance"), () => {
frm.call({
method: "enqueue_job",
doc: frm.doc,
freeze: true,
callback: () => {
frm.reload_doc();
}
})
})
}
},
regenerate_closing_balance(frm) {
if (frm.doc.status == "Completed") {
frm.add_custom_button(__("Regenerate Closing Stock Balance"), () => {
frm.call({
method: "regenerate_closing_balance",
doc: frm.doc,
freeze: true,
callback: () => {
frm.reload_doc();
}
})
})
}
}
});

View File

@ -0,0 +1,148 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "naming_series:",
"creation": "2023-05-17 09:58:42.086911",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"naming_series",
"company",
"status",
"column_break_p0s0",
"from_date",
"to_date",
"filters_section",
"item_code",
"item_group",
"include_uom",
"column_break_rm5w",
"warehouse",
"warehouse_type",
"amended_from"
],
"fields": [
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Naming Series",
"options": "CBAL-.#####"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company"
},
{
"default": "Draft",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"in_preview": 1,
"label": "Status",
"options": "Draft\nQueued\nIn Progress\nCompleted\nFailed\nCanceled",
"read_only": 1
},
{
"fieldname": "column_break_p0s0",
"fieldtype": "Column Break"
},
{
"fieldname": "from_date",
"fieldtype": "Date",
"label": "From Date"
},
{
"fieldname": "to_date",
"fieldtype": "Date",
"label": "To Date"
},
{
"collapsible": 1,
"fieldname": "filters_section",
"fieldtype": "Section Break",
"label": "Filters"
},
{
"fieldname": "item_code",
"fieldtype": "Link",
"label": "Item Code",
"options": "Item"
},
{
"fieldname": "item_group",
"fieldtype": "Link",
"label": "Item Group",
"options": "Item Group"
},
{
"fieldname": "column_break_rm5w",
"fieldtype": "Column Break"
},
{
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"options": "Warehouse"
},
{
"fieldname": "warehouse_type",
"fieldtype": "Link",
"label": "Warehouse Type",
"options": "Warehouse Type"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Closing Stock Balance",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Closing Stock Balance",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "include_uom",
"fieldtype": "Link",
"label": "Include UOM",
"options": "UOM"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-05-17 11:46:04.448220",
"modified_by": "Administrator",
"module": "Stock",
"name": "Closing Stock Balance",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,133 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import json
import frappe
from frappe import _
from frappe.core.doctype.prepared_report.prepared_report import create_json_gz_file
from frappe.desk.form.load import get_attachments
from frappe.model.document import Document
from frappe.utils import get_link_to_form, gzip_decompress, parse_json
from frappe.utils.background_jobs import enqueue
from erpnext.stock.report.stock_balance.stock_balance import execute
class ClosingStockBalance(Document):
def before_save(self):
self.set_status()
def set_status(self, save=False):
self.status = "Queued"
if self.docstatus == 2:
self.status = "Canceled"
if self.docstatus == 0:
self.status = "Draft"
if save:
self.db_set("status", self.status)
def validate(self):
self.validate_duplicate()
def validate_duplicate(self):
table = frappe.qb.DocType("Closing Stock Balance")
query = (
frappe.qb.from_(table)
.select(table.name)
.where(
(table.docstatus == 1)
& (table.company == self.company)
& (
(table.from_date.between(self.from_date, self.to_date))
| (table.to_date.between(self.from_date, self.to_date))
| (table.from_date >= self.from_date and table.to_date <= self.to_date)
)
)
)
for fieldname in ["warehouse", "item_code", "item_group", "warehouse_type"]:
if self.get(fieldname):
query = query.where(table.get(fieldname) == self.get(fieldname))
query = query.run(as_dict=True)
if query and query[0].name:
name = get_link_to_form("Closing Stock Balance", query[0].name)
msg = f"Closing Stock Balance {name} already exists for the selected date range"
frappe.throw(_(msg), title=_("Duplicate Closing Stock Balance"))
def on_submit(self):
self.set_status(save=True)
self.enqueue_job()
def on_cancel(self):
self.set_status(save=True)
self.clear_attachment()
@frappe.whitelist()
def enqueue_job(self):
self.db_set("status", "In Progress")
self.clear_attachment()
enqueue(prepare_closing_stock_balance, name=self.name, queue="long", timeout=1500)
@frappe.whitelist()
def regenerate_closing_balance(self):
self.enqueue_job()
def clear_attachment(self):
if attachments := get_attachments(self.doctype, self.name):
attachment = attachments[0]
frappe.delete_doc("File", attachment.name)
def create_closing_stock_balance_entries(self):
columns, data = execute(
filters=frappe._dict(
{
"company": self.company,
"from_date": self.from_date,
"to_date": self.to_date,
"warehouse": self.warehouse,
"item_code": self.item_code,
"item_group": self.item_group,
"warehouse_type": self.warehouse_type,
"include_uom": self.include_uom,
"ignore_closing_balance": 1,
"show_variant_attributes": 1,
"show_stock_ageing_data": 1,
}
)
)
create_json_gz_file({"columns": columns, "data": data}, self.doctype, self.name)
def get_prepared_data(self):
if attachments := get_attachments(self.doctype, self.name):
attachment = attachments[0]
attached_file = frappe.get_doc("File", attachment.name)
data = gzip_decompress(attached_file.get_content())
if data := json.loads(data.decode("utf-8")):
data = data
return parse_json(data)
return frappe._dict({})
def prepare_closing_stock_balance(name):
doc = frappe.get_doc("Closing Stock Balance", name)
doc.db_set("status", "In Progress")
try:
doc.create_closing_stock_balance_entries()
doc.db_set("status", "Completed")
except Exception as e:
doc.db_set("status", "Failed")
traceback = frappe.get_traceback()
frappe.log_error("Closing Stock Balance Failed", traceback, doc.doctype, doc.name)

View File

@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestClosingStockBalance(FrappeTestCase):
pass

View File

@ -185,11 +185,14 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends erpn
}
if(doc.docstatus==0 && !doc.__islocal) {
this.frm.add_custom_button(__('Packing Slip'), function() {
frappe.model.open_mapped_doc({
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_packing_slip",
frm: me.frm
}) }, __('Create'));
if (doc.__onload && doc.__onload.has_unpacked_items) {
this.frm.add_custom_button(__('Packing Slip'), function() {
frappe.model.open_mapped_doc({
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_packing_slip",
frm: me.frm
}) }, __('Create')
);
}
}
if (!doc.__islocal && doc.docstatus==1) {

View File

@ -86,6 +86,10 @@ class DeliveryNote(SellingController):
]
)
def onload(self):
if self.docstatus == 0:
self.set_onload("has_unpacked_items", self.has_unpacked_items())
def before_print(self, settings=None):
def toggle_print_hide(meta, fieldname):
df = meta.get_field(fieldname)
@ -147,6 +151,8 @@ class DeliveryNote(SellingController):
if not self.installation_status:
self.installation_status = "Not Installed"
self.validate_against_stock_reservation_entries()
self.reset_default_field_value("set_warehouse", "items", "warehouse")
def validate_with_previous_doc(self):
@ -239,6 +245,8 @@ class DeliveryNote(SellingController):
self.update_prevdoc_status()
self.update_billing_status()
self.update_stock_reservation_entries()
if not self.is_return:
self.check_credit_limit()
elif self.issue_credit_note:
@ -268,6 +276,90 @@ class DeliveryNote(SellingController):
self.repost_future_sle_and_gle()
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
def update_stock_reservation_entries(self) -> None:
"""Updates Delivered Qty in Stock Reservation Entries."""
# Don't update Delivered Qty on Return or Cancellation.
if self.is_return or self._action == "cancel":
return
for item in self.get("items"):
# Skip if `Sales Order` or `Sales Order Item` reference is not set.
if not item.against_sales_order or not item.so_detail:
continue
sre_list = frappe.db.get_all(
"Stock Reservation Entry",
{
"docstatus": 1,
"voucher_type": "Sales Order",
"voucher_no": item.against_sales_order,
"voucher_detail_no": item.so_detail,
"warehouse": item.warehouse,
"status": ["not in", ["Delivered", "Cancelled"]],
},
order_by="creation",
)
# Skip if no Stock Reservation Entries.
if not sre_list:
continue
available_qty_to_deliver = item.stock_qty
for sre in sre_list:
if available_qty_to_deliver <= 0:
break
sre_doc = frappe.get_doc("Stock Reservation Entry", sre)
# `Delivered Qty` should be less than or equal to `Reserved Qty`.
qty_to_be_deliver = min(sre_doc.reserved_qty - sre_doc.delivered_qty, available_qty_to_deliver)
sre_doc.delivered_qty += qty_to_be_deliver
sre_doc.db_update()
# Update Stock Reservation Entry `Status` based on `Delivered Qty`.
sre_doc.update_status()
available_qty_to_deliver -= qty_to_be_deliver
def validate_against_stock_reservation_entries(self):
"""Validates if Stock Reservation Entries are available for the Sales Order Item reference."""
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
get_sre_reserved_qty_details_for_voucher_detail_no,
)
# Don't validate if Return
if self.is_return:
return
for item in self.get("items"):
# Skip if `Sales Order` or `Sales Order Item` reference is not set.
if not item.against_sales_order or not item.so_detail:
continue
sre_data = get_sre_reserved_qty_details_for_voucher_detail_no(
"Sales Order", item.against_sales_order, item.so_detail
)
# Skip if stock is not reserved.
if not sre_data:
continue
# Set `Warehouse` from SRE if not set.
if not item.warehouse:
item.warehouse = sre_data[0]
else:
# Throw if `Warehouse` is different from SRE.
if item.warehouse != sre_data[0]:
frappe.throw(
_("Row #{0}: Stock is reserved for Item {1} in Warehouse {2}.").format(
item.idx, frappe.bold(item.item_code), frappe.bold(sre_data[0])
),
title=_("Stock Reservation Warehouse Mismatch"),
)
def check_credit_limit(self):
from erpnext.selling.doctype.customer.customer import check_credit_limit
@ -302,20 +394,21 @@ class DeliveryNote(SellingController):
)
def validate_packed_qty(self):
"""
Validate that if packed qty exists, it should be equal to qty
"""
if not any(flt(d.get("packed_qty")) for d in self.get("items")):
return
has_error = False
for d in self.get("items"):
if flt(d.get("qty")) != flt(d.get("packed_qty")):
frappe.msgprint(
_("Packed quantity must equal quantity for Item {0} in row {1}").format(d.item_code, d.idx)
)
has_error = True
if has_error:
raise frappe.ValidationError
"""Validate that if packed qty exists, it should be equal to qty"""
if frappe.db.exists("Packing Slip", {"docstatus": 1, "delivery_note": self.name}):
product_bundle_list = self.get_product_bundle_list()
for item in self.items + self.packed_items:
if (
item.item_code not in product_bundle_list
and flt(item.packed_qty)
and flt(item.packed_qty) != flt(item.qty)
):
frappe.throw(
_("Row {0}: Packed Qty must be equal to {1} Qty.").format(
item.idx, frappe.bold(item.doctype)
)
)
def update_pick_list_status(self):
from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status
@ -393,6 +486,23 @@ class DeliveryNote(SellingController):
)
)
def has_unpacked_items(self):
product_bundle_list = self.get_product_bundle_list()
for item in self.items + self.packed_items:
if item.item_code not in product_bundle_list and flt(item.packed_qty) < flt(item.qty):
return True
return False
def get_product_bundle_list(self):
items_list = [item.item_code for item in self.items]
return frappe.db.get_all(
"Product Bundle",
filters={"new_item_code": ["in", items_list]},
pluck="name",
)
def update_billed_amount_based_on_so(so_detail, update_modified=True):
from frappe.query_builder.functions import Sum
@ -684,6 +794,12 @@ def make_installation_note(source_name, target_doc=None):
@frappe.whitelist()
def make_packing_slip(source_name, target_doc=None):
def set_missing_values(source, target):
target.run_method("set_missing_values")
def update_item(obj, target, source_parent):
target.qty = flt(obj.qty) - flt(obj.packed_qty)
doclist = get_mapped_doc(
"Delivery Note",
source_name,
@ -698,12 +814,34 @@ def make_packing_slip(source_name, target_doc=None):
"field_map": {
"item_code": "item_code",
"item_name": "item_name",
"batch_no": "batch_no",
"description": "description",
"qty": "qty",
"stock_uom": "stock_uom",
"name": "dn_detail",
},
"postprocess": update_item,
"condition": lambda item: (
not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code})
and flt(item.packed_qty) < flt(item.qty)
),
},
"Packed Item": {
"doctype": "Packing Slip Item",
"field_map": {
"item_code": "item_code",
"item_name": "item_name",
"batch_no": "batch_no",
"description": "description",
"qty": "qty",
"name": "pi_detail",
},
"postprocess": update_item,
"condition": lambda item: (flt(item.packed_qty) < flt(item.qty)),
},
},
target_doc,
set_missing_values,
)
return doclist

View File

@ -84,6 +84,7 @@
"installed_qty",
"item_tax_rate",
"column_break_atna",
"packed_qty",
"received_qty",
"accounting_details_section",
"expense_account",
@ -850,6 +851,16 @@
"print_hide": 1,
"read_only": 1,
"report_hide": 1
},
{
"default": "0",
"depends_on": "eval: doc.packed_qty",
"fieldname": "packed_qty",
"fieldtype": "Float",
"label": "Packed Qty",
"no_copy": 1,
"non_negative": 1,
"read_only": 1
}
],
"idx": 1,

View File

@ -619,6 +619,16 @@ def make_stock_entry(source_name, target_doc=None):
target.stock_entry_type = target.purpose
target.set_job_card_data()
if source.job_card:
job_card_details = frappe.get_all(
"Job Card", filters={"name": source.job_card}, fields=["bom_no", "for_quantity"]
)
if job_card_details and job_card_details[0]:
target.bom_no = job_card_details[0].bom_no
target.fg_completed_qty = job_card_details[0].for_quantity
target.from_bom = 1
doclist = get_mapped_doc(
"Material Request",
source_name,

View File

@ -27,6 +27,7 @@
"actual_qty",
"projected_qty",
"ordered_qty",
"packed_qty",
"column_break_16",
"incoming_rate",
"picked_qty",
@ -242,13 +243,23 @@
"label": "Picked Qty",
"no_copy": 1,
"read_only": 1
},
{
"default": "0",
"depends_on": "eval: doc.packed_qty",
"fieldname": "packed_qty",
"fieldtype": "Float",
"label": "Packed Qty",
"no_copy": 1,
"non_negative": 1,
"read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-04-27 05:23:08.683245",
"modified": "2023-04-28 13:16:38.460806",
"modified_by": "Administrator",
"module": "Stock",
"name": "Packed Item",

View File

@ -1,113 +1,46 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
cur_frm.fields_dict['delivery_note'].get_query = function(doc, cdt, cdn) {
return{
filters:{ 'docstatus': 0}
}
}
frappe.ui.form.on('Packing Slip', {
setup: (frm) => {
frm.set_query('delivery_note', () => {
return {
filters: {
docstatus: 0,
}
}
});
frm.set_query('item_code', 'items', (doc, cdt, cdn) => {
if (!doc.delivery_note) {
frappe.throw(__('Please select a Delivery Note'));
} else {
let d = locals[cdt][cdn];
return {
query: 'erpnext.stock.doctype.packing_slip.packing_slip.item_details',
filters: {
delivery_note: doc.delivery_note,
}
}
}
});
},
cur_frm.fields_dict['items'].grid.get_field('item_code').get_query = function(doc, cdt, cdn) {
if(!doc.delivery_note) {
frappe.throw(__("Please select a Delivery Note"));
} else {
return {
query: "erpnext.stock.doctype.packing_slip.packing_slip.item_details",
filters:{ 'delivery_note': doc.delivery_note}
refresh: (frm) => {
frm.toggle_display('misc_details', frm.doc.amended_from);
},
delivery_note: (frm) => {
frm.set_value('items', null);
if (frm.doc.delivery_note) {
erpnext.utils.map_current_doc({
method: 'erpnext.stock.doctype.delivery_note.delivery_note.make_packing_slip',
source_name: frm.doc.delivery_note,
target_doc: frm,
freeze: true,
freeze_message: __('Creating Packing Slip ...'),
});
}
}
}
cur_frm.cscript.onload_post_render = function(doc, cdt, cdn) {
if(doc.delivery_note && doc.__islocal) {
cur_frm.cscript.get_items(doc, cdt, cdn);
}
}
cur_frm.cscript.get_items = function(doc, cdt, cdn) {
return this.frm.call({
doc: this.frm.doc,
method: "get_items",
callback: function(r) {
if(!r.exc) cur_frm.refresh();
}
});
}
cur_frm.cscript.refresh = function(doc, dt, dn) {
cur_frm.toggle_display("misc_details", doc.amended_from);
}
cur_frm.cscript.validate = function(doc, cdt, cdn) {
cur_frm.cscript.validate_case_nos(doc);
cur_frm.cscript.validate_calculate_item_details(doc);
}
// To Case No. cannot be less than From Case No.
cur_frm.cscript.validate_case_nos = function(doc) {
doc = locals[doc.doctype][doc.name];
if(cint(doc.from_case_no)==0) {
frappe.msgprint(__("The 'From Package No.' field must neither be empty nor it's value less than 1."));
frappe.validated = false;
} else if(!cint(doc.to_case_no)) {
doc.to_case_no = doc.from_case_no;
refresh_field('to_case_no');
} else if(cint(doc.to_case_no) < cint(doc.from_case_no)) {
frappe.msgprint(__("'To Case No.' cannot be less than 'From Case No.'"));
frappe.validated = false;
}
}
cur_frm.cscript.validate_calculate_item_details = function(doc) {
doc = locals[doc.doctype][doc.name];
var ps_detail = doc.items || [];
cur_frm.cscript.validate_duplicate_items(doc, ps_detail);
cur_frm.cscript.calc_net_total_pkg(doc, ps_detail);
}
// Do not allow duplicate items i.e. items with same item_code
// Also check for 0 qty
cur_frm.cscript.validate_duplicate_items = function(doc, ps_detail) {
for(var i=0; i<ps_detail.length; i++) {
for(var j=0; j<ps_detail.length; j++) {
if(i!=j && ps_detail[i].item_code && ps_detail[i].item_code==ps_detail[j].item_code) {
frappe.msgprint(__("You have entered duplicate items. Please rectify and try again."));
frappe.validated = false;
return;
}
}
if(flt(ps_detail[i].qty)<=0) {
frappe.msgprint(__("Invalid quantity specified for item {0}. Quantity should be greater than 0.", [ps_detail[i].item_code]));
frappe.validated = false;
}
}
}
// Calculate Net Weight of Package
cur_frm.cscript.calc_net_total_pkg = function(doc, ps_detail) {
var net_weight_pkg = 0;
doc.net_weight_uom = (ps_detail && ps_detail.length) ? ps_detail[0].weight_uom : '';
doc.gross_weight_uom = doc.net_weight_uom;
for(var i=0; i<ps_detail.length; i++) {
var item = ps_detail[i];
if(item.weight_uom != doc.net_weight_uom) {
frappe.msgprint(__("Different UOM for items will lead to incorrect (Total) Net Weight value. Make sure that Net Weight of each item is in the same UOM."));
frappe.validated = false;
}
net_weight_pkg += flt(item.net_weight) * flt(item.qty);
}
doc.net_weight_pkg = roundNumber(net_weight_pkg, 2);
if(!flt(doc.gross_weight_pkg)) {
doc.gross_weight_pkg = doc.net_weight_pkg;
}
refresh_many(['net_weight_pkg', 'net_weight_uom', 'gross_weight_uom', 'gross_weight_pkg']);
}
// TODO: validate gross weight field
},
});

View File

@ -1,264 +1,262 @@
{
"allow_import": 1,
"autoname": "MAT-PAC-.YYYY.-.#####",
"creation": "2013-04-11 15:32:24",
"description": "Generate packing slips for packages to be delivered. Used to notify package number, package contents and its weight.",
"doctype": "DocType",
"document_type": "Document",
"engine": "InnoDB",
"field_order": [
"packing_slip_details",
"column_break0",
"delivery_note",
"column_break1",
"naming_series",
"section_break0",
"column_break2",
"from_case_no",
"column_break3",
"to_case_no",
"package_item_details",
"get_items",
"items",
"package_weight_details",
"net_weight_pkg",
"net_weight_uom",
"column_break4",
"gross_weight_pkg",
"gross_weight_uom",
"letter_head_details",
"letter_head",
"misc_details",
"amended_from"
],
"fields": [
{
"fieldname": "packing_slip_details",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break0",
"fieldtype": "Column Break"
},
{
"description": "Indicates that the package is a part of this delivery (Only Draft)",
"fieldname": "delivery_note",
"fieldtype": "Link",
"in_global_search": 1,
"in_list_view": 1,
"label": "Delivery Note",
"options": "Delivery Note",
"reqd": 1
},
{
"fieldname": "column_break1",
"fieldtype": "Column Break"
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Series",
"options": "MAT-PAC-.YYYY.-",
"print_hide": 1,
"reqd": 1,
"set_only_once": 1
},
{
"fieldname": "section_break0",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break2",
"fieldtype": "Column Break"
},
{
"description": "Identification of the package for the delivery (for print)",
"fieldname": "from_case_no",
"fieldtype": "Int",
"in_list_view": 1,
"label": "From Package No.",
"no_copy": 1,
"reqd": 1,
"width": "50px"
},
{
"fieldname": "column_break3",
"fieldtype": "Column Break"
},
{
"description": "If more than one package of the same type (for print)",
"fieldname": "to_case_no",
"fieldtype": "Int",
"in_list_view": 1,
"label": "To Package No.",
"no_copy": 1,
"width": "50px"
},
{
"fieldname": "package_item_details",
"fieldtype": "Section Break"
},
{
"fieldname": "get_items",
"fieldtype": "Button",
"label": "Get Items"
},
{
"fieldname": "items",
"fieldtype": "Table",
"label": "Items",
"options": "Packing Slip Item",
"reqd": 1
},
{
"fieldname": "package_weight_details",
"fieldtype": "Section Break",
"label": "Package Weight Details"
},
{
"description": "The net weight of this package. (calculated automatically as sum of net weight of items)",
"fieldname": "net_weight_pkg",
"fieldtype": "Float",
"label": "Net Weight",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "net_weight_uom",
"fieldtype": "Link",
"label": "Net Weight UOM",
"no_copy": 1,
"options": "UOM",
"read_only": 1
},
{
"fieldname": "column_break4",
"fieldtype": "Column Break"
},
{
"description": "The gross weight of the package. Usually net weight + packaging material weight. (for print)",
"fieldname": "gross_weight_pkg",
"fieldtype": "Float",
"label": "Gross Weight",
"no_copy": 1
},
{
"fieldname": "gross_weight_uom",
"fieldtype": "Link",
"label": "Gross Weight UOM",
"no_copy": 1,
"options": "UOM"
},
{
"fieldname": "letter_head_details",
"fieldtype": "Section Break",
"label": "Letter Head"
},
{
"allow_on_submit": 1,
"fieldname": "letter_head",
"fieldtype": "Link",
"label": "Letter Head",
"options": "Letter Head",
"print_hide": 1
},
{
"fieldname": "misc_details",
"fieldtype": "Section Break"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Amended From",
"no_copy": 1,
"options": "Packing Slip",
"print_hide": 1,
"read_only": 1
}
],
"icon": "fa fa-suitcase",
"idx": 1,
"is_submittable": 1,
"modified": "2019-09-09 04:45:08.082862",
"modified_by": "Administrator",
"module": "Stock",
"name": "Packing Slip",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Stock User",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales User",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Item Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Stock Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"search_fields": "delivery_note",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC"
"actions": [],
"allow_import": 1,
"autoname": "MAT-PAC-.YYYY.-.#####",
"creation": "2013-04-11 15:32:24",
"description": "Generate packing slips for packages to be delivered. Used to notify package number, package contents and its weight.",
"doctype": "DocType",
"document_type": "Document",
"engine": "InnoDB",
"field_order": [
"packing_slip_details",
"column_break0",
"delivery_note",
"column_break1",
"naming_series",
"section_break0",
"column_break2",
"from_case_no",
"column_break3",
"to_case_no",
"package_item_details",
"items",
"package_weight_details",
"net_weight_pkg",
"net_weight_uom",
"column_break4",
"gross_weight_pkg",
"gross_weight_uom",
"letter_head_details",
"letter_head",
"misc_details",
"amended_from"
],
"fields": [
{
"fieldname": "packing_slip_details",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break0",
"fieldtype": "Column Break"
},
{
"description": "Indicates that the package is a part of this delivery (Only Draft)",
"fieldname": "delivery_note",
"fieldtype": "Link",
"in_global_search": 1,
"in_list_view": 1,
"label": "Delivery Note",
"options": "Delivery Note",
"reqd": 1
},
{
"fieldname": "column_break1",
"fieldtype": "Column Break"
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Series",
"options": "MAT-PAC-.YYYY.-",
"print_hide": 1,
"reqd": 1,
"set_only_once": 1
},
{
"fieldname": "section_break0",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break2",
"fieldtype": "Column Break"
},
{
"description": "Identification of the package for the delivery (for print)",
"fieldname": "from_case_no",
"fieldtype": "Int",
"in_list_view": 1,
"label": "From Package No.",
"no_copy": 1,
"reqd": 1,
"width": "50px"
},
{
"fieldname": "column_break3",
"fieldtype": "Column Break"
},
{
"description": "If more than one package of the same type (for print)",
"fieldname": "to_case_no",
"fieldtype": "Int",
"in_list_view": 1,
"label": "To Package No.",
"no_copy": 1,
"width": "50px"
},
{
"fieldname": "package_item_details",
"fieldtype": "Section Break"
},
{
"fieldname": "items",
"fieldtype": "Table",
"label": "Items",
"options": "Packing Slip Item",
"reqd": 1
},
{
"fieldname": "package_weight_details",
"fieldtype": "Section Break",
"label": "Package Weight Details"
},
{
"description": "The net weight of this package. (calculated automatically as sum of net weight of items)",
"fieldname": "net_weight_pkg",
"fieldtype": "Float",
"label": "Net Weight",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "net_weight_uom",
"fieldtype": "Link",
"label": "Net Weight UOM",
"no_copy": 1,
"options": "UOM",
"read_only": 1
},
{
"fieldname": "column_break4",
"fieldtype": "Column Break"
},
{
"description": "The gross weight of the package. Usually net weight + packaging material weight. (for print)",
"fieldname": "gross_weight_pkg",
"fieldtype": "Float",
"label": "Gross Weight",
"no_copy": 1
},
{
"fieldname": "gross_weight_uom",
"fieldtype": "Link",
"label": "Gross Weight UOM",
"no_copy": 1,
"options": "UOM"
},
{
"fieldname": "letter_head_details",
"fieldtype": "Section Break",
"label": "Letter Head"
},
{
"allow_on_submit": 1,
"fieldname": "letter_head",
"fieldtype": "Link",
"label": "Letter Head",
"options": "Letter Head",
"print_hide": 1
},
{
"fieldname": "misc_details",
"fieldtype": "Section Break"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Amended From",
"no_copy": 1,
"options": "Packing Slip",
"print_hide": 1,
"read_only": 1
}
],
"icon": "fa fa-suitcase",
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-04-28 18:01:37.341619",
"modified_by": "Administrator",
"module": "Stock",
"name": "Packing Slip",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Stock User",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales User",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Item Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Stock Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"search_fields": "delivery_note",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -4,193 +4,181 @@
import frappe
from frappe import _
from frappe.model import no_value_fields
from frappe.model.document import Document
from frappe.utils import cint, flt
from erpnext.controllers.status_updater import StatusUpdater
class PackingSlip(Document):
def validate(self):
"""
* Validate existence of submitted Delivery Note
* Case nos do not overlap
* Check if packed qty doesn't exceed actual qty of delivery note
It is necessary to validate case nos before checking quantity
"""
self.validate_delivery_note()
self.validate_items_mandatory()
self.validate_case_nos()
self.validate_qty()
class PackingSlip(StatusUpdater):
def __init__(self, *args, **kwargs) -> None:
super(PackingSlip, self).__init__(*args, **kwargs)
self.status_updater = [
{
"target_dt": "Delivery Note Item",
"join_field": "dn_detail",
"target_field": "packed_qty",
"target_parent_dt": "Delivery Note",
"target_ref_field": "qty",
"source_dt": "Packing Slip Item",
"source_field": "qty",
},
{
"target_dt": "Packed Item",
"join_field": "pi_detail",
"target_field": "packed_qty",
"target_parent_dt": "Delivery Note",
"target_ref_field": "qty",
"source_dt": "Packing Slip Item",
"source_field": "qty",
},
]
def validate(self) -> None:
from erpnext.utilities.transaction_base import validate_uom_is_integer
self.validate_delivery_note()
self.validate_case_nos()
self.validate_items()
validate_uom_is_integer(self, "stock_uom", "qty")
validate_uom_is_integer(self, "weight_uom", "net_weight")
def validate_delivery_note(self):
"""
Validates if delivery note has status as draft
"""
if cint(frappe.db.get_value("Delivery Note", self.delivery_note, "docstatus")) != 0:
frappe.throw(_("Delivery Note {0} must not be submitted").format(self.delivery_note))
self.set_missing_values()
self.calculate_net_total_pkg()
def validate_items_mandatory(self):
rows = [d.item_code for d in self.get("items")]
if not rows:
frappe.msgprint(_("No Items to pack"), raise_exception=1)
def on_submit(self):
self.update_prevdoc_status()
def on_cancel(self):
self.update_prevdoc_status()
def validate_delivery_note(self):
"""Raises an exception if the `Delivery Note` status is not Draft"""
if cint(frappe.db.get_value("Delivery Note", self.delivery_note, "docstatus")) != 0:
frappe.throw(
_("A Packing Slip can only be created for Draft Delivery Note.").format(self.delivery_note)
)
def validate_case_nos(self):
"""
Validate if case nos overlap. If they do, recommend next case no.
"""
if not cint(self.from_case_no):
frappe.msgprint(_("Please specify a valid 'From Case No.'"), raise_exception=1)
"""Validate if case nos overlap. If they do, recommend next case no."""
if cint(self.from_case_no) <= 0:
frappe.throw(
_("The 'From Package No.' field must neither be empty nor it's value less than 1.")
)
elif not self.to_case_no:
self.to_case_no = self.from_case_no
elif cint(self.from_case_no) > cint(self.to_case_no):
frappe.msgprint(_("'To Case No.' cannot be less than 'From Case No.'"), raise_exception=1)
elif cint(self.to_case_no) < cint(self.from_case_no):
frappe.throw(_("'To Package No.' cannot be less than 'From Package No.'"))
else:
ps = frappe.qb.DocType("Packing Slip")
res = (
frappe.qb.from_(ps)
.select(
ps.name,
)
.where(
(ps.delivery_note == self.delivery_note)
& (ps.docstatus == 1)
& (
(ps.from_case_no.between(self.from_case_no, self.to_case_no))
| (ps.to_case_no.between(self.from_case_no, self.to_case_no))
| ((ps.from_case_no <= self.from_case_no) & (ps.to_case_no >= self.from_case_no))
)
)
).run()
res = frappe.db.sql(
"""SELECT name FROM `tabPacking Slip`
WHERE delivery_note = %(delivery_note)s AND docstatus = 1 AND
((from_case_no BETWEEN %(from_case_no)s AND %(to_case_no)s)
OR (to_case_no BETWEEN %(from_case_no)s AND %(to_case_no)s)
OR (%(from_case_no)s BETWEEN from_case_no AND to_case_no))
""",
{
"delivery_note": self.delivery_note,
"from_case_no": self.from_case_no,
"to_case_no": self.to_case_no,
},
)
if res:
frappe.throw(
_("""Package No(s) already in use. Try from Package No {0}""").format(
self.get_recommended_case_no()
)
)
if res:
frappe.throw(
_("""Case No(s) already in use. Try from Case No {0}""").format(self.get_recommended_case_no())
def validate_items(self):
for item in self.items:
if item.qty <= 0:
frappe.throw(_("Row {0}: Qty must be greater than 0.").format(item.idx))
if not item.dn_detail and not item.pi_detail:
frappe.throw(
_("Row {0}: Either Delivery Note Item or Packed Item reference is mandatory.").format(
item.idx
)
)
remaining_qty = frappe.db.get_value(
"Delivery Note Item" if item.dn_detail else "Packed Item",
{"name": item.dn_detail or item.pi_detail, "docstatus": 0},
["sum(qty - packed_qty)"],
)
def validate_qty(self):
"""Check packed qty across packing slips and delivery note"""
# Get Delivery Note Items, Item Quantity Dict and No. of Cases for this Packing slip
dn_details, ps_item_qty, no_of_cases = self.get_details_for_packing()
if remaining_qty is None:
frappe.throw(
_("Row {0}: Please provide a valid Delivery Note Item or Packed Item reference.").format(
item.idx
)
)
elif remaining_qty <= 0:
frappe.throw(
_("Row {0}: Packing Slip is already created for Item {1}.").format(
item.idx, frappe.bold(item.item_code)
)
)
elif item.qty > remaining_qty:
frappe.throw(
_("Row {0}: Qty cannot be greater than {1} for the Item {2}.").format(
item.idx, frappe.bold(remaining_qty), frappe.bold(item.item_code)
)
)
for item in dn_details:
new_packed_qty = (flt(ps_item_qty[item["item_code"]]) * no_of_cases) + flt(item["packed_qty"])
if new_packed_qty > flt(item["qty"]) and no_of_cases:
self.recommend_new_qty(item, ps_item_qty, no_of_cases)
def get_details_for_packing(self):
"""
Returns
* 'Delivery Note Items' query result as a list of dict
* Item Quantity dict of current packing slip doc
* No. of Cases of this packing slip
"""
rows = [d.item_code for d in self.get("items")]
# also pick custom fields from delivery note
custom_fields = ", ".join(
"dni.`{0}`".format(d.fieldname)
for d in frappe.get_meta("Delivery Note Item").get_custom_fields()
if d.fieldtype not in no_value_fields
)
if custom_fields:
custom_fields = ", " + custom_fields
condition = ""
if rows:
condition = " and item_code in (%s)" % (", ".join(["%s"] * len(rows)))
# gets item code, qty per item code, latest packed qty per item code and stock uom
res = frappe.db.sql(
"""select item_code, sum(qty) as qty,
(select sum(psi.qty * (abs(ps.to_case_no - ps.from_case_no) + 1))
from `tabPacking Slip` ps, `tabPacking Slip Item` psi
where ps.name = psi.parent and ps.docstatus = 1
and ps.delivery_note = dni.parent and psi.item_code=dni.item_code) as packed_qty,
stock_uom, item_name, description, dni.batch_no {custom_fields}
from `tabDelivery Note Item` dni
where parent=%s {condition}
group by item_code""".format(
condition=condition, custom_fields=custom_fields
),
tuple([self.delivery_note] + rows),
as_dict=1,
)
ps_item_qty = dict([[d.item_code, d.qty] for d in self.get("items")])
no_of_cases = cint(self.to_case_no) - cint(self.from_case_no) + 1
return res, ps_item_qty, no_of_cases
def recommend_new_qty(self, item, ps_item_qty, no_of_cases):
"""
Recommend a new quantity and raise a validation exception
"""
item["recommended_qty"] = (flt(item["qty"]) - flt(item["packed_qty"])) / no_of_cases
item["specified_qty"] = flt(ps_item_qty[item["item_code"]])
if not item["packed_qty"]:
item["packed_qty"] = 0
frappe.throw(
_("Quantity for Item {0} must be less than {1}").format(
item.get("item_code"), item.get("recommended_qty")
)
)
def update_item_details(self):
"""
Fill empty columns in Packing Slip Item
"""
def set_missing_values(self):
if not self.from_case_no:
self.from_case_no = self.get_recommended_case_no()
for d in self.get("items"):
res = frappe.db.get_value("Item", d.item_code, ["weight_per_unit", "weight_uom"], as_dict=True)
for item in self.items:
stock_uom, weight_per_unit, weight_uom = frappe.db.get_value(
"Item", item.item_code, ["stock_uom", "weight_per_unit", "weight_uom"]
)
if res and len(res) > 0:
d.net_weight = res["weight_per_unit"]
d.weight_uom = res["weight_uom"]
item.stock_uom = stock_uom
if weight_per_unit and not item.net_weight:
item.net_weight = weight_per_unit
if weight_uom and not item.weight_uom:
item.weight_uom = weight_uom
def get_recommended_case_no(self):
"""
Returns the next case no. for a new packing slip for a delivery
note
"""
recommended_case_no = frappe.db.sql(
"""SELECT MAX(to_case_no) FROM `tabPacking Slip`
WHERE delivery_note = %s AND docstatus=1""",
self.delivery_note,
"""Returns the next case no. for a new packing slip for a delivery note"""
return (
cint(
frappe.db.get_value(
"Packing Slip", {"delivery_note": self.delivery_note, "docstatus": 1}, ["max(to_case_no)"]
)
)
+ 1
)
return cint(recommended_case_no[0][0]) + 1
def calculate_net_total_pkg(self):
self.net_weight_uom = self.items[0].weight_uom if self.items else None
self.gross_weight_uom = self.net_weight_uom
@frappe.whitelist()
def get_items(self):
self.set("items", [])
net_weight_pkg = 0
for item in self.items:
if item.weight_uom != self.net_weight_uom:
frappe.throw(
_(
"Different UOM for items will lead to incorrect (Total) Net Weight value. Make sure that Net Weight of each item is in the same UOM."
)
)
custom_fields = frappe.get_meta("Delivery Note Item").get_custom_fields()
net_weight_pkg += flt(item.net_weight) * flt(item.qty)
dn_details = self.get_details_for_packing()[0]
for item in dn_details:
if flt(item.qty) > flt(item.packed_qty):
ch = self.append("items", {})
ch.item_code = item.item_code
ch.item_name = item.item_name
ch.stock_uom = item.stock_uom
ch.description = item.description
ch.batch_no = item.batch_no
ch.qty = flt(item.qty) - flt(item.packed_qty)
self.net_weight_pkg = round(net_weight_pkg, 2)
# copy custom fields
for d in custom_fields:
if item.get(d.fieldname):
ch.set(d.fieldname, item.get(d.fieldname))
self.update_item_details()
if not flt(self.gross_weight_pkg):
self.gross_weight_pkg = self.net_weight_pkg
@frappe.whitelist()

View File

@ -3,9 +3,118 @@
import unittest
# test_records = frappe.get_test_records('Packing Slip')
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
from erpnext.stock.doctype.delivery_note.delivery_note import make_packing_slip
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.item.test_item import make_item
class TestPackingSlip(unittest.TestCase):
pass
class TestPackingSlip(FrappeTestCase):
def test_packing_slip(self):
# Step - 1: Create a Product Bundle
items = create_items()
make_product_bundle(items[0], items[1:], 5)
# Step - 2: Create a Delivery Note (Draft) with Product Bundle
dn = create_delivery_note(
item_code=items[0],
qty=2,
do_not_save=True,
)
dn.append(
"items",
{
"item_code": items[1],
"warehouse": "_Test Warehouse - _TC",
"qty": 10,
},
)
dn.save()
# Step - 3: Make a Packing Slip from Delivery Note for 4 Qty
ps1 = make_packing_slip(dn.name)
for item in ps1.items:
item.qty = 4
ps1.save()
ps1.submit()
# Test - 1: `Packed Qty` should be updated to 4 in Delivery Note Items and Packed Items.
dn.load_from_db()
for item in dn.items:
if not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}):
self.assertEqual(item.packed_qty, 4)
for item in dn.packed_items:
self.assertEqual(item.packed_qty, 4)
# Step - 4: Make another Packing Slip from Delivery Note for 6 Qty
ps2 = make_packing_slip(dn.name)
ps2.save()
ps2.submit()
# Test - 2: `Packed Qty` should be updated to 10 in Delivery Note Items and Packed Items.
dn.load_from_db()
for item in dn.items:
if not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}):
self.assertEqual(item.packed_qty, 10)
for item in dn.packed_items:
self.assertEqual(item.packed_qty, 10)
# Step - 5: Cancel Packing Slip [1]
ps1.cancel()
# Test - 3: `Packed Qty` should be updated to 4 in Delivery Note Items and Packed Items.
dn.load_from_db()
for item in dn.items:
if not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}):
self.assertEqual(item.packed_qty, 6)
for item in dn.packed_items:
self.assertEqual(item.packed_qty, 6)
# Step - 6: Cancel Packing Slip [2]
ps2.cancel()
# Test - 4: `Packed Qty` should be updated to 0 in Delivery Note Items and Packed Items.
dn.load_from_db()
for item in dn.items:
if not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}):
self.assertEqual(item.packed_qty, 0)
for item in dn.packed_items:
self.assertEqual(item.packed_qty, 0)
# Step - 7: Make Packing Slip for more Qty than Delivery Note
ps3 = make_packing_slip(dn.name)
ps3.items[0].qty = 20
# Test - 5: Should throw an ValidationError, as Packing Slip Qty is more than Delivery Note Qty
self.assertRaises(frappe.exceptions.ValidationError, ps3.save)
# Step - 8: Make Packing Slip for less Qty than Delivery Note
ps4 = make_packing_slip(dn.name)
ps4.items[0].qty = 5
ps4.save()
ps4.submit()
# Test - 6: Delivery Note should throw a ValidationError on Submit, as Packed Qty and Delivery Note Qty are not the same
dn.load_from_db()
self.assertRaises(frappe.exceptions.ValidationError, dn.submit)
def create_items():
items_properties = [
{"is_stock_item": 0},
{"is_stock_item": 1, "stock_uom": "Nos"},
{"is_stock_item": 1, "stock_uom": "Box"},
]
items = []
for properties in items_properties:
items.append(make_item(properties=properties).name)
return items

View File

@ -20,7 +20,8 @@
"stock_uom",
"weight_uom",
"page_break",
"dn_detail"
"dn_detail",
"pi_detail"
],
"fields": [
{
@ -121,13 +122,23 @@
"fieldtype": "Data",
"hidden": 1,
"in_list_view": 1,
"label": "DN Detail"
"label": "Delivery Note Item",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "pi_detail",
"fieldtype": "Data",
"hidden": 1,
"label": "Delivery Note Packed Item",
"no_copy": 1,
"read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-12-14 01:22:00.715935",
"modified": "2023-04-28 15:00:14.079306",
"modified_by": "Administrator",
"module": "Stock",
"name": "Packing Slip Item",
@ -136,5 +147,6 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -460,7 +460,7 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus)
item_doc.qty if (docstatus == 1 and item_doc.stock_qty == 0) else item_doc.stock_qty
)
while remaining_stock_qty > 0 and available_locations:
while flt(remaining_stock_qty) > 0 and available_locations:
item_location = available_locations.pop(0)
item_location = frappe._dict(item_location)

View File

@ -59,6 +59,7 @@ frappe.ui.form.on('Repost Item Valuation', {
if (frm.doc.status == 'In Progress') {
frm.doc.current_index = data.current_index;
frm.doc.items_to_be_repost = data.items_to_be_repost;
frm.doc.total_reposting_count = data.total_reposting_count;
frm.dashboard.reset();
frm.trigger('show_reposting_progress');
@ -95,6 +96,11 @@ frappe.ui.form.on('Repost Item Valuation', {
var bars = [];
let total_count = frm.doc.items_to_be_repost ? JSON.parse(frm.doc.items_to_be_repost).length : 0;
if (frm.doc?.total_reposting_count) {
total_count = frm.doc.total_reposting_count;
}
let progress = flt(cint(frm.doc.current_index) / total_count * 100, 2) || 0.5;
var title = __('Reposting Completed {0}%', [progress]);

View File

@ -22,11 +22,15 @@
"amended_from",
"error_section",
"error_log",
"reposting_info_section",
"reposting_data_file",
"items_to_be_repost",
"affected_transactions",
"distinct_item_and_warehouse",
"column_break_o1sj",
"total_reposting_count",
"current_index",
"gl_reposting_index"
"gl_reposting_index",
"affected_transactions"
],
"fields": [
{
@ -191,13 +195,36 @@
"fieldtype": "Int",
"hidden": 1,
"label": "GL reposting index",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "reposting_info_section",
"fieldtype": "Section Break",
"label": "Reposting Info"
},
{
"fieldname": "column_break_o1sj",
"fieldtype": "Column Break"
},
{
"fieldname": "total_reposting_count",
"fieldtype": "Int",
"label": "Total Reposting Count",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "reposting_data_file",
"fieldtype": "Attach",
"label": "Reposting Data File",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-11-28 16:00:05.637440",
"modified": "2023-05-31 12:48:57.138693",
"modified_by": "Administrator",
"module": "Stock",
"name": "Repost Item Valuation",

View File

@ -3,6 +3,7 @@
import frappe
from frappe import _
from frappe.desk.form.load import get_attachments
from frappe.exceptions import QueryDeadlockError, QueryTimeoutError
from frappe.model.document import Document
from frappe.query_builder import DocType, Interval
@ -95,6 +96,12 @@ class RepostItemValuation(Document):
self.allow_negative_stock = 1
def on_cancel(self):
self.clear_attachment()
def on_trash(self):
self.clear_attachment()
def set_company(self):
if self.based_on == "Transaction":
self.company = frappe.get_cached_value(self.voucher_type, self.voucher_no, "company")
@ -110,6 +117,14 @@ class RepostItemValuation(Document):
if write:
self.db_set("status", self.status)
def clear_attachment(self):
if attachments := get_attachments(self.doctype, self.name):
attachment = attachments[0]
frappe.delete_doc("File", attachment.name)
if self.reposting_data_file:
self.db_set("reposting_data_file", None)
def on_submit(self):
"""During tests reposts are executed immediately.

View File

@ -2398,7 +2398,7 @@ def move_sample_to_retention_warehouse(company, items):
"basic_rate": item.get("valuation_rate"),
"uom": item.get("uom"),
"stock_uom": item.get("stock_uom"),
"conversion_factor": 1.0,
"conversion_factor": item.get("conversion_factor") or 1.0,
"serial_no": sample_serial_nos,
"batch_no": item.get("batch_no"),
},

View File

@ -47,6 +47,7 @@ class StockReconciliation(StockController):
self.validate_putaway_capacity()
if self._action == "submit":
self.validate_reserved_stock()
self.make_batches("warehouse")
def on_submit(self):
@ -60,6 +61,7 @@ class StockReconciliation(StockController):
def on_cancel(self):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
self.validate_reserved_stock()
self.make_sle_on_cancel()
self.make_gl_entries_on_cancel()
self.repost_future_sle_and_gle()
@ -224,6 +226,46 @@ class StockReconciliation(StockController):
except Exception as e:
self.validation_messages.append(_("Row #") + " " + ("%d: " % (row.idx)) + cstr(e))
def validate_reserved_stock(self) -> None:
"""Raises an exception if there is any reserved stock for the items in the Stock Reconciliation."""
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
get_sre_reserved_qty_details_for_item_and_warehouse as get_sre_reserved_qty_details,
)
item_code_list, warehouse_list = [], []
for item in self.items:
item_code_list.append(item.item_code)
warehouse_list.append(item.warehouse)
sre_reserved_qty_details = get_sre_reserved_qty_details(item_code_list, warehouse_list)
if sre_reserved_qty_details:
data = []
for (item_code, warehouse), reserved_qty in sre_reserved_qty_details.items():
data.append([item_code, warehouse, reserved_qty])
msg = ""
if len(data) == 1:
msg = _(
"{0} units are reserved for Item {1} in Warehouse {2}, please un-reserve the same to {3} the Stock Reconciliation."
).format(bold(data[0][2]), bold(data[0][0]), bold(data[0][1]), self._action)
else:
items_html = ""
for d in data:
items_html += "<li>{0} units of Item {1} in Warehouse {2}</li>".format(
bold(d[2]), bold(d[0]), bold(d[1])
)
msg = _(
"The stock has been reserved for the following Items and Warehouses, un-reserve the same to {0} the Stock Reconciliation: <br /><br /> {1}"
).format(self._action, items_html)
frappe.throw(
msg,
title=_("Stock Reservation"),
)
def update_stock_ledger(self):
"""find difference between current and expected entries
and create stock ledger entries based on the difference"""

View File

@ -0,0 +1,8 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Stock Reservation Entry", {
refresh(frm) {
frm.page.btn_primary.hide()
},
});

View File

@ -0,0 +1,234 @@
{
"actions": [],
"allow_copy": 1,
"autoname": "MAT-SRE-.YYYY.-.#####",
"creation": "2023-03-20 10:45:59.258959",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"item_code",
"warehouse",
"column_break_elik",
"voucher_type",
"voucher_no",
"voucher_detail_no",
"section_break_xt4m",
"available_qty",
"voucher_qty",
"stock_uom",
"column_break_o6ex",
"reserved_qty",
"delivered_qty",
"section_break_3vb3",
"company",
"column_break_jbyr",
"project",
"status",
"amended_from"
],
"fields": [
{
"fieldname": "item_code",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Item Code",
"oldfieldname": "item_code",
"oldfieldtype": "Link",
"options": "Item",
"print_width": "100px",
"read_only": 1,
"search_index": 1,
"width": "100px"
},
{
"fieldname": "warehouse",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Warehouse",
"oldfieldname": "warehouse",
"oldfieldtype": "Link",
"options": "Warehouse",
"print_width": "100px",
"read_only": 1,
"search_index": 1,
"width": "100px"
},
{
"fieldname": "voucher_type",
"fieldtype": "Select",
"in_filter": 1,
"label": "Voucher Type",
"oldfieldname": "voucher_type",
"oldfieldtype": "Data",
"options": "\nSales Order",
"print_width": "150px",
"read_only": 1,
"width": "150px"
},
{
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"in_filter": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Voucher No",
"oldfieldname": "voucher_no",
"oldfieldtype": "Data",
"options": "voucher_type",
"print_width": "150px",
"read_only": 1,
"width": "150px"
},
{
"fieldname": "voucher_detail_no",
"fieldtype": "Data",
"label": "Voucher Detail No",
"oldfieldname": "voucher_detail_no",
"oldfieldtype": "Data",
"print_width": "150px",
"read_only": 1,
"search_index": 1,
"width": "150px"
},
{
"fieldname": "stock_uom",
"fieldtype": "Link",
"label": "Stock UOM",
"oldfieldname": "stock_uom",
"oldfieldtype": "Data",
"options": "UOM",
"print_width": "150px",
"read_only": 1,
"width": "150px"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project",
"read_only": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_filter": 1,
"label": "Company",
"oldfieldname": "company",
"oldfieldtype": "Data",
"options": "Company",
"print_width": "150px",
"read_only": 1,
"search_index": 1,
"width": "150px"
},
{
"fieldname": "reserved_qty",
"fieldtype": "Float",
"in_filter": 1,
"in_list_view": 1,
"label": "Reserved Qty",
"oldfieldname": "actual_qty",
"oldfieldtype": "Currency",
"print_width": "150px",
"read_only": 1,
"width": "150px"
},
{
"default": "Draft",
"fieldname": "status",
"fieldtype": "Select",
"hidden": 1,
"label": "Status",
"options": "Draft\nPartially Reserved\nReserved\nPartially Delivered\nDelivered\nCancelled",
"read_only": 1
},
{
"default": "0",
"fieldname": "delivered_qty",
"fieldtype": "Float",
"label": "Delivered Qty",
"read_only": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Stock Reservation Entry",
"print_hide": 1,
"read_only": 1
},
{
"default": "0",
"fieldname": "available_qty",
"fieldtype": "Float",
"label": "Available Qty to Reserve",
"no_copy": 1,
"read_only": 1
},
{
"default": "0",
"fieldname": "voucher_qty",
"fieldtype": "Float",
"label": "Voucher Qty",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "column_break_elik",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_xt4m",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_o6ex",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_3vb3",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_jbyr",
"fieldtype": "Column Break"
}
],
"hide_toolbar": 1,
"in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-03-29 18:36:26.752872",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reservation Entry",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,312 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder.functions import Sum
class StockReservationEntry(Document):
def validate(self) -> None:
from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company
self.validate_mandatory()
self.validate_for_group_warehouse()
validate_disabled_warehouse(self.warehouse)
validate_warehouse_company(self.warehouse, self.company)
def on_submit(self) -> None:
self.update_reserved_qty_in_voucher()
self.update_status()
def on_cancel(self) -> None:
self.update_reserved_qty_in_voucher()
self.update_status()
def validate_mandatory(self) -> None:
"""Raises exception if mandatory fields are not set."""
mandatory = [
"item_code",
"warehouse",
"voucher_type",
"voucher_no",
"voucher_detail_no",
"available_qty",
"voucher_qty",
"stock_uom",
"reserved_qty",
"company",
]
for d in mandatory:
if not self.get(d):
frappe.throw(_("{0} is required").format(self.meta.get_label(d)))
def validate_for_group_warehouse(self) -> None:
"""Raises exception if `Warehouse` is a Group Warehouse."""
if frappe.get_cached_value("Warehouse", self.warehouse, "is_group"):
frappe.throw(
_("Stock cannot be reserved in group warehouse {0}.").format(frappe.bold(self.warehouse)),
title=_("Invalid Warehouse"),
)
def update_status(self, status: str = None, update_modified: bool = True) -> None:
"""Updates status based on Voucher Qty, Reserved Qty and Delivered Qty."""
if not status:
if self.docstatus == 2:
status = "Cancelled"
elif self.docstatus == 1:
if self.reserved_qty == self.delivered_qty:
status = "Delivered"
elif self.delivered_qty and self.delivered_qty < self.reserved_qty:
status = "Partially Delivered"
elif self.reserved_qty == self.voucher_qty:
status = "Reserved"
else:
status = "Partially Reserved"
else:
status = "Draft"
frappe.db.set_value(self.doctype, self.name, "status", status, update_modified=update_modified)
def update_reserved_qty_in_voucher(
self, reserved_qty_field: str = "stock_reserved_qty", update_modified: bool = True
) -> None:
"""Updates total reserved qty in the voucher."""
item_doctype = "Sales Order Item" if self.voucher_type == "Sales Order" else None
if item_doctype:
sre = frappe.qb.DocType("Stock Reservation Entry")
reserved_qty = (
frappe.qb.from_(sre)
.select(Sum(sre.reserved_qty))
.where(
(sre.docstatus == 1)
& (sre.voucher_type == self.voucher_type)
& (sre.voucher_no == self.voucher_no)
& (sre.voucher_detail_no == self.voucher_detail_no)
)
).run(as_list=True)[0][0] or 0
frappe.db.set_value(
item_doctype,
self.voucher_detail_no,
reserved_qty_field,
reserved_qty,
update_modified=update_modified,
)
def validate_stock_reservation_settings(voucher: object) -> None:
"""Raises an exception if `Stock Reservation` is not enabled or `Voucher Type` is not allowed."""
if not frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"):
frappe.throw(
_("Please enable {0} in the {1}.").format(
frappe.bold("Stock Reservation"), frappe.bold("Stock Settings")
)
)
# Voucher types allowed for stock reservation
allowed_voucher_types = ["Sales Order"]
if voucher.doctype not in allowed_voucher_types:
frappe.throw(
_("Stock Reservation can only be created against {0}.").format(", ".join(allowed_voucher_types))
)
def get_available_qty_to_reserve(item_code: str, warehouse: str) -> float:
"""Returns `Available Qty to Reserve (Actual Qty - Reserved Qty)` for Item and Warehouse combination."""
from erpnext.stock.utils import get_stock_balance
available_qty = get_stock_balance(item_code, warehouse)
if available_qty:
sre = frappe.qb.DocType("Stock Reservation Entry")
reserved_qty = (
frappe.qb.from_(sre)
.select(Sum(sre.reserved_qty - sre.delivered_qty))
.where(
(sre.docstatus == 1)
& (sre.item_code == item_code)
& (sre.warehouse == warehouse)
& (sre.status.notin(["Delivered", "Cancelled"]))
)
).run()[0][0] or 0.0
if reserved_qty:
return available_qty - reserved_qty
return available_qty
def get_stock_reservation_entries_for_voucher(
voucher_type: str, voucher_no: str, voucher_detail_no: str = None, fields: list[str] = None
) -> list[dict]:
"""Returns list of Stock Reservation Entries against a Voucher."""
if not fields or not isinstance(fields, list):
fields = [
"name",
"item_code",
"warehouse",
"voucher_detail_no",
"reserved_qty",
"delivered_qty",
"stock_uom",
]
sre = frappe.qb.DocType("Stock Reservation Entry")
query = (
frappe.qb.from_(sre)
.where(
(sre.docstatus == 1)
& (sre.voucher_type == voucher_type)
& (sre.voucher_no == voucher_no)
& (sre.status.notin(["Delivered", "Cancelled"]))
)
.orderby(sre.creation)
)
for field in fields:
query = query.select(sre[field])
if voucher_detail_no:
query = query.where(sre.voucher_detail_no == voucher_detail_no)
return query.run(as_dict=True)
def get_sre_reserved_qty_details_for_item_and_warehouse(
item_code_list: list, warehouse_list: list
) -> dict:
"""Returns a dict like {("item_code", "warehouse"): "reserved_qty", ... }."""
sre_details = {}
if item_code_list and warehouse_list:
sre = frappe.qb.DocType("Stock Reservation Entry")
sre_data = (
frappe.qb.from_(sre)
.select(
sre.item_code,
sre.warehouse,
Sum(sre.reserved_qty - sre.delivered_qty).as_("reserved_qty"),
)
.where(
(sre.docstatus == 1)
& (sre.item_code.isin(item_code_list))
& (sre.warehouse.isin(warehouse_list))
& (sre.status.notin(["Delivered", "Cancelled"]))
)
.groupby(sre.item_code, sre.warehouse)
).run(as_dict=True)
if sre_data:
sre_details = {(d["item_code"], d["warehouse"]): d["reserved_qty"] for d in sre_data}
return sre_details
def get_sre_reserved_qty_for_item_and_warehouse(item_code: str, warehouse: str) -> float:
"""Returns `Reserved Qty` for Item and Warehouse combination."""
reserved_qty = 0.0
if item_code and warehouse:
sre = frappe.qb.DocType("Stock Reservation Entry")
return (
frappe.qb.from_(sre)
.select(Sum(sre.reserved_qty - sre.delivered_qty))
.where(
(sre.docstatus == 1)
& (sre.item_code == item_code)
& (sre.warehouse == warehouse)
& (sre.status.notin(["Delivered", "Cancelled"]))
)
).run(as_list=True)[0][0] or 0.0
return reserved_qty
def get_sre_reserved_qty_details_for_voucher(voucher_type: str, voucher_no: str) -> dict:
"""Returns a dict like {"voucher_detail_no": "reserved_qty", ... }."""
sre = frappe.qb.DocType("Stock Reservation Entry")
data = (
frappe.qb.from_(sre)
.select(
sre.voucher_detail_no,
(Sum(sre.reserved_qty) - Sum(sre.delivered_qty)).as_("reserved_qty"),
)
.where(
(sre.docstatus == 1)
& (sre.voucher_type == voucher_type)
& (sre.voucher_no == voucher_no)
& (sre.status.notin(["Delivered", "Cancelled"]))
)
.groupby(sre.voucher_detail_no)
).run(as_list=True)
return frappe._dict(data)
def get_sre_reserved_qty_details_for_voucher_detail_no(
voucher_type: str, voucher_no: str, voucher_detail_no: str
) -> list:
"""Returns a list like ["warehouse", "reserved_qty"]."""
sre = frappe.qb.DocType("Stock Reservation Entry")
reserved_qty_details = (
frappe.qb.from_(sre)
.select(sre.warehouse, (Sum(sre.reserved_qty) - Sum(sre.delivered_qty)))
.where(
(sre.docstatus == 1)
& (sre.voucher_type == voucher_type)
& (sre.voucher_no == voucher_no)
& (sre.voucher_detail_no == voucher_detail_no)
& (sre.status.notin(["Delivered", "Cancelled"]))
)
.orderby(sre.creation)
.groupby(sre.warehouse)
).run(as_list=True)
if reserved_qty_details:
return reserved_qty_details[0]
return reserved_qty_details
def has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: str = None) -> bool:
"""Returns True if there is any Stock Reservation Entry for the given voucher."""
if get_stock_reservation_entries_for_voucher(
voucher_type, voucher_no, voucher_detail_no, fields=["name"]
):
return True
return False
@frappe.whitelist()
def cancel_stock_reservation_entries(
voucher_type: str, voucher_no: str, voucher_detail_no: str = None, notify: bool = True
) -> None:
"""Cancel Stock Reservation Entries for the given voucher."""
sre_list = get_stock_reservation_entries_for_voucher(
voucher_type, voucher_no, voucher_detail_no, fields=["name"]
)
if sre_list:
for sre in sre_list:
frappe.get_doc("Stock Reservation Entry", sre.name).cancel()
if notify:
frappe.msgprint(_("Stock Reservation Entries Cancelled"), alert=True, indicator="red")

View File

@ -0,0 +1,16 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.listview_settings['Stock Reservation Entry'] = {
get_indicator: function (doc) {
const status_colors = {
'Draft': 'red',
'Partially Reserved': 'orange',
'Reserved': 'blue',
'Partially Delivered': 'purple',
'Delivered': 'green',
'Cancelled': 'red',
};
return [__(doc.status), status_colors[doc.status], 'status,=,' + doc.status];
},
};

View File

@ -0,0 +1,335 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase, change_settings
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.stock_entry.stock_entry import StockEntry
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.stock.utils import get_stock_balance
class TestStockReservationEntry(FrappeTestCase):
def setUp(self) -> None:
self.items = create_items()
create_material_receipt(self.items)
def tearDown(self) -> None:
return super().tearDown()
def test_validate_stock_reservation_settings(self) -> None:
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
validate_stock_reservation_settings,
)
voucher = frappe._dict(
{
"doctype": "Sales Order",
}
)
# Case - 1: When `Stock Reservation` is disabled in `Stock Settings`, throw `ValidationError`
with change_settings("Stock Settings", {"enable_stock_reservation": 0}):
self.assertRaises(frappe.ValidationError, validate_stock_reservation_settings, voucher)
with change_settings("Stock Settings", {"enable_stock_reservation": 1}):
# Case - 2: When `Voucher Type` is not allowed for `Stock Reservation`, throw `ValidationError`
voucher.doctype = "NOT ALLOWED"
self.assertRaises(frappe.ValidationError, validate_stock_reservation_settings, voucher)
# Case - 3: When `Voucher Type` is allowed for `Stock Reservation`
voucher.doctype = "Sales Order"
self.assertIsNone(validate_stock_reservation_settings(voucher), None)
def test_get_available_qty_to_reserve(self) -> None:
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
get_available_qty_to_reserve,
)
item_code, warehouse = "SR Item 1", "_Test Warehouse - _TC"
# Case - 1: When `Reserved Qty` is `0`, Available Qty to Reserve = Actual Qty
cancel_all_stock_reservation_entries()
available_qty_to_reserve = get_available_qty_to_reserve(item_code, warehouse)
expected_available_qty_to_reserve = get_stock_balance(item_code, warehouse)
self.assertEqual(available_qty_to_reserve, expected_available_qty_to_reserve)
# Case - 2: When `Reserved Qty` is `> 0`, Available Qty to Reserve = Actual Qty - Reserved Qty
sre = make_stock_reservation_entry(
item_code=item_code,
warehouse=warehouse,
ignore_validate=True,
)
available_qty_to_reserve = get_available_qty_to_reserve(item_code, warehouse)
expected_available_qty_to_reserve = get_stock_balance(item_code, warehouse) - sre.reserved_qty
self.assertEqual(available_qty_to_reserve, expected_available_qty_to_reserve)
def test_update_status(self) -> None:
sre = make_stock_reservation_entry(
reserved_qty=30,
ignore_validate=True,
do_not_submit=True,
)
# Draft: When DocStatus is `0`
sre.load_from_db()
self.assertEqual(sre.status, "Draft")
# Partially Reserved: When DocStatus is `1` and `Reserved Qty` < `Voucher Qty`
sre.submit()
sre.load_from_db()
self.assertEqual(sre.status, "Partially Reserved")
# Reserved: When DocStatus is `1` and `Reserved Qty` = `Voucher Qty`
sre.reserved_qty = sre.voucher_qty
sre.db_update()
sre.update_status()
sre.load_from_db()
self.assertEqual(sre.status, "Reserved")
# Partially Delivered: When DocStatus is `1` and (0 < `Delivered Qty` < `Voucher Qty`)
sre.delivered_qty = 10
sre.db_update()
sre.update_status()
sre.load_from_db()
self.assertEqual(sre.status, "Partially Delivered")
# Delivered: When DocStatus is `1` and `Delivered Qty` = `Voucher Qty`
sre.delivered_qty = sre.voucher_qty
sre.db_update()
sre.update_status()
sre.load_from_db()
self.assertEqual(sre.status, "Delivered")
# Cancelled: When DocStatus is `2`
sre.cancel()
sre.load_from_db()
self.assertEqual(sre.status, "Cancelled")
@change_settings("Stock Settings", {"enable_stock_reservation": 1})
def test_update_reserved_qty_in_voucher(self) -> None:
item_code, warehouse = "SR Item 1", "_Test Warehouse - _TC"
# Step - 1: Create a `Sales Order`
so = make_sales_order(
item_code=item_code,
warehouse=warehouse,
qty=50,
rate=100,
do_not_submit=True,
)
so.reserve_stock = 0 # Stock Reservation Entries won't be created on submit
so.items[0].reserve_stock = 1
so.save()
so.submit()
# Step - 2: Create a `Stock Reservation Entry[1]` for the `Sales Order Item`
sre1 = make_stock_reservation_entry(
item_code=item_code,
warehouse=warehouse,
voucher_type="Sales Order",
voucher_no=so.name,
voucher_detail_no=so.items[0].name,
reserved_qty=30,
)
so.load_from_db()
sre1.load_from_db()
self.assertEqual(sre1.status, "Partially Reserved")
self.assertEqual(so.items[0].stock_reserved_qty, sre1.reserved_qty)
# Step - 3: Create a `Stock Reservation Entry[2]` for the `Sales Order Item`
sre2 = make_stock_reservation_entry(
item_code=item_code,
warehouse=warehouse,
voucher_type="Sales Order",
voucher_no=so.name,
voucher_detail_no=so.items[0].name,
reserved_qty=20,
)
so.load_from_db()
sre2.load_from_db()
self.assertEqual(sre1.status, "Partially Reserved")
self.assertEqual(so.items[0].stock_reserved_qty, sre1.reserved_qty + sre2.reserved_qty)
# Step - 4: Cancel `Stock Reservation Entry[1]`
sre1.cancel()
so.load_from_db()
sre1.load_from_db()
self.assertEqual(sre1.status, "Cancelled")
self.assertEqual(so.items[0].stock_reserved_qty, sre2.reserved_qty)
# Step - 5: Cancel `Stock Reservation Entry[2]`
sre2.cancel()
so.load_from_db()
sre2.load_from_db()
self.assertEqual(sre1.status, "Cancelled")
self.assertEqual(so.items[0].stock_reserved_qty, 0)
@change_settings("Stock Settings", {"enable_stock_reservation": 1})
def test_cant_consume_reserved_stock(self) -> None:
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
cancel_stock_reservation_entries,
)
from erpnext.stock.stock_ledger import NegativeStockError
item_code, warehouse = "SR Item 1", "_Test Warehouse - _TC"
# Step - 1: Create a `Sales Order`
so = make_sales_order(
item_code=item_code,
warehouse=warehouse,
qty=50,
rate=100,
do_not_submit=True,
)
so.reserve_stock = 1 # Stock Reservation Entries will be created on submit
so.items[0].reserve_stock = 1
so.save()
so.submit()
actual_qty = get_stock_balance(item_code, warehouse)
# Step - 2: Try to consume (Transfer/Issue/Deliver) the Available Qty via Stock Entry or Delivery Note, should throw `NegativeStockError`.
se = make_stock_entry(
item_code=item_code,
qty=actual_qty,
from_warehouse=warehouse,
rate=100,
purpose="Material Issue",
do_not_submit=True,
)
self.assertRaises(NegativeStockError, se.submit)
se.cancel()
# Step - 3: Unreserve the stock and consume the Available Qty via Stock Entry.
cancel_stock_reservation_entries(so.doctype, so.name)
se = make_stock_entry(
item_code=item_code,
qty=actual_qty,
from_warehouse=warehouse,
rate=100,
purpose="Material Issue",
do_not_submit=True,
)
se.submit()
se.cancel()
def create_items() -> dict:
from erpnext.stock.doctype.item.test_item import make_item
items_details = {
# Stock Items
"SR Item 1": {"is_stock_item": 1, "valuation_rate": 100},
"SR Item 2": {"is_stock_item": 1, "valuation_rate": 200, "stock_uom": "Kg"},
# Batch Items
"SR Batch Item 1": {
"is_stock_item": 1,
"valuation_rate": 100,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "SRBI-1-.#####.",
},
"SR Batch Item 2": {
"is_stock_item": 1,
"valuation_rate": 200,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "SRBI-2-.#####.",
"stock_uom": "Kg",
},
# Serial Item
"SR Serial Item 1": {
"is_stock_item": 1,
"valuation_rate": 100,
"has_serial_no": 1,
"serial_no_series": "SRSI-1-.#####",
},
# Batch and Serial Item
"SR Batch and Serial Item 1": {
"is_stock_item": 1,
"valuation_rate": 100,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "SRBSI-1-.#####.",
"has_serial_no": 1,
"serial_no_series": "SRBSI-1-.#####",
},
}
items = {}
for item_code, properties in items_details.items():
items[item_code] = make_item(item_code, properties)
return items
def create_material_receipt(
items: dict, warehouse: str = "_Test Warehouse - _TC", qty: float = 100
) -> StockEntry:
se = frappe.new_doc("Stock Entry")
se.purpose = "Material Receipt"
se.company = "_Test Company"
cost_center = frappe.get_value("Company", se.company, "cost_center")
expense_account = frappe.get_value("Company", se.company, "stock_adjustment_account")
for item in items.values():
se.append(
"items",
{
"item_code": item.item_code,
"t_warehouse": warehouse,
"qty": qty,
"basic_rate": item.valuation_rate or 100,
"conversion_factor": 1.0,
"transfer_qty": qty,
"cost_center": cost_center,
"expense_account": expense_account,
},
)
se.set_stock_entry_type()
se.insert()
se.submit()
return se
def cancel_all_stock_reservation_entries() -> None:
sre_list = frappe.db.get_all("Stock Reservation Entry", filters={"docstatus": 1}, pluck="name")
for sre in sre_list:
frappe.get_doc("Stock Reservation Entry", sre).cancel()
def make_stock_reservation_entry(**args):
doc = frappe.new_doc("Stock Reservation Entry")
args = frappe._dict(args)
doc.item_code = args.item_code or "SR Item 1"
doc.warehouse = args.warehouse or "_Test Warehouse - _TC"
doc.voucher_type = args.voucher_type
doc.voucher_no = args.voucher_no
doc.voucher_detail_no = args.voucher_detail_no
doc.available_qty = args.available_qty or 100
doc.voucher_qty = args.voucher_qty or 50
doc.stock_uom = args.stock_uom or "Nos"
doc.reserved_qty = args.reserved_qty or 50
doc.delivered_qty = args.delivered_qty or 0
doc.company = args.company or "_Test Company"
if args.ignore_validate:
doc.flags.ignore_validate = True
if not args.do_not_save:
doc.save()
if not args.do_not_submit:
doc.submit()
return doc

View File

@ -8,12 +8,12 @@
"defaults_tab",
"item_defaults_section",
"item_naming_by",
"valuation_method",
"item_group",
"stock_uom",
"column_break_4",
"default_warehouse",
"sample_retention_warehouse",
"valuation_method",
"stock_uom",
"price_list_defaults_section",
"auto_insert_price_list_rate_if_missing",
"column_break_12",
@ -31,6 +31,11 @@
"action_if_quality_inspection_is_not_submitted",
"column_break_23",
"action_if_quality_inspection_is_rejected",
"stock_reservation_tab",
"enable_stock_reservation",
"column_break_rx3e",
"reserve_stock_on_sales_order_submission",
"allow_partial_reservation",
"serial_and_batch_item_settings_tab",
"section_break_7",
"automatically_set_serial_nos_based_on_fifo",
@ -96,6 +101,7 @@
"fieldtype": "Column Break"
},
{
"documentation_url": "https://docs.erpnext.com/docs/v14/user/manual/en/stock/articles/calculation-of-valuation-rate-in-fifo-and-moving-average",
"fieldname": "valuation_method",
"fieldtype": "Select",
"label": "Default Valuation Method",
@ -339,6 +345,37 @@
{
"fieldname": "column_break_121",
"fieldtype": "Column Break"
},
{
"fieldname": "stock_reservation_tab",
"fieldtype": "Tab Break",
"label": "Stock Reservation"
},
{
"default": "0",
"fieldname": "enable_stock_reservation",
"fieldtype": "Check",
"label": "Enable Stock Reservation"
},
{
"default": "0",
"depends_on": "eval: doc.enable_stock_reservation",
"description": "If enabled, <b>Stock Reservation Entries</b> will be created on submission of <b>Sales Order</b>",
"fieldname": "reserve_stock_on_sales_order_submission",
"fieldtype": "Check",
"label": "Reserve Stock on Sales Order Submission"
},
{
"fieldname": "column_break_rx3e",
"fieldtype": "Column Break"
},
{
"default": "1",
"depends_on": "eval: doc.enable_stock_reservation",
"description": "If enabled, <b>Partial Stock Reservation Entries</b> can be created. For example, If you have a <b>Sales Order</b> of 100 units and the Available Stock is 90 units then a Stock Reservation Entry will be created for 90 units. ",
"fieldname": "allow_partial_reservation",
"fieldtype": "Check",
"label": "Allow Partial Reservation"
}
],
"icon": "icon-cog",
@ -346,7 +383,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2022-02-05 15:33:43.692736",
"modified": "2023-05-29 15:09:54.959411",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",

Some files were not shown because too many files have changed in this diff Show More