Merge pull request #35473 from frappe/version-14-hotfix

chore: release v14
This commit is contained in:
Deepesh Garg
2023-05-31 11:44:01 +05:30
committed by GitHub
50 changed files with 2033 additions and 1197 deletions

2
.github/stale.yml vendored
View File

@@ -13,7 +13,7 @@ exemptProjects: true
exemptMilestones: true
pulls:
daysUntilStale: 15
daysUntilStale: 14
daysUntilClose: 3
exemptLabels:
- hotfix

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

@@ -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
@@ -673,18 +674,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

@@ -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

@@ -736,7 +736,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:

View File

@@ -337,10 +337,6 @@ class Asset(AccountsController):
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, -finance_book.frequency_of_depreciation + 1)
# if asset is being sold
if date_of_disposal:
from_date = self.get_from_date_for_disposal(finance_book)
@@ -363,14 +359,20 @@ class Asset(AccountsController):
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(
self.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(self.available_for_use_date, -1)
depreciation_amount, days, months = self.get_pro_rata_amt(
finance_book,
depreciation_amount,
from_date,
finance_book.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(self.available_for_use_date),
(self.number_of_depreciations_booked * finance_book.frequency_of_depreciation),
)
depreciation_amount, days, months = self.get_pro_rata_amt(
finance_book,
depreciation_amount,
@@ -378,10 +380,6 @@ class Asset(AccountsController):
finance_book.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(finance_book.depreciation_start_date, -months + 1)
# For last row
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
@@ -406,9 +404,7 @@ class Asset(AccountsController):
depreciation_amount_without_pro_rata, depreciation_amount, finance_book.finance_book
)
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

@@ -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

@@ -184,6 +184,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

@@ -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."

View File

@@ -342,6 +342,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

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

@@ -331,3 +331,4 @@ execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_h
# below migration patches should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
erpnext.patches.v14_0.update_company_in_ldc
erpnext.patches.v14_0.set_packed_qty_in_draft_delivery_notes

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

@@ -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

@@ -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)
@@ -299,20 +303,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
@@ -390,6 +395,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
@@ -681,6 +703,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,
@@ -695,12 +723,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,
@@ -866,4 +877,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

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

@@ -2401,7 +2401,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

@@ -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",
@@ -96,6 +96,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",
@@ -346,7 +347,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",

View File

@@ -17,6 +17,7 @@ frappe.ui.form.on("Warehouse", {
return {
filters: {
is_group: 1,
company: doc.company,
},
};
});
@@ -39,26 +40,34 @@ frappe.ui.form.on("Warehouse", {
!frm.doc.__islocal
);
if (!frm.doc.__islocal) {
if (!frm.is_new()) {
frappe.contacts.render_address_and_contact(frm);
let enable_toggle = frm.doc.disabled ? "Enable" : "Disable";
frm.add_custom_button(__(enable_toggle), () => {
frm.set_value('disabled', 1 - frm.doc.disabled);
frm.save()
});
frm.add_custom_button(__("Stock Balance"), function () {
frappe.set_route("query-report", "Stock Balance", {
warehouse: frm.doc.name,
});
});
frm.add_custom_button(
frm.doc.is_group
? __("Convert to Ledger", null, "Warehouse")
: __("Convert to Group", null, "Warehouse"),
function () {
convert_to_group_or_ledger(frm);
},
);
} else {
frappe.contacts.clear_address_and_contact(frm);
}
frm.add_custom_button(__("Stock Balance"), function () {
frappe.set_route("query-report", "Stock Balance", {
warehouse: frm.doc.name,
});
});
frm.add_custom_button(
frm.doc.is_group
? __("Convert to Ledger", null, "Warehouse")
: __("Convert to Group", null, "Warehouse"),
function () {
convert_to_group_or_ledger(frm);
},
);
if (!frm.doc.is_group && frm.doc.__onload && frm.doc.__onload.account) {
frm.add_custom_button(

View File

@@ -1,23 +1,21 @@
{
"actions": [],
"allow_import": 1,
"creation": "2013-03-07 18:50:32",
"creation": "2023-05-29 13:02:17.121296",
"description": "A logical Warehouse against which stock entries are made.",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
"warehouse_detail",
"disabled",
"warehouse_name",
"column_break_3",
"warehouse_type",
"parent_warehouse",
"default_in_transit_warehouse",
"is_group",
"parent_warehouse",
"column_break_4",
"account",
"company",
"disabled",
"address_and_contact",
"address_html",
"column_break_10",
@@ -32,6 +30,10 @@
"city",
"state",
"pin",
"transit_section",
"warehouse_type",
"column_break_qajx",
"default_in_transit_warehouse",
"tree_details",
"lft",
"rgt",
@@ -58,7 +60,7 @@
"fieldname": "is_group",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Is Group"
"label": "Is Group Warehouse"
},
{
"fieldname": "company",
@@ -78,7 +80,7 @@
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"in_list_view": 1,
"hidden": 1,
"label": "Disabled"
},
{
@@ -164,7 +166,6 @@
{
"fieldname": "city",
"fieldtype": "Data",
"in_list_view": 1,
"label": "City",
"oldfieldname": "city",
"oldfieldtype": "Data"
@@ -238,13 +239,23 @@
"fieldtype": "Link",
"label": "Default In-Transit Warehouse",
"options": "Warehouse"
},
{
"collapsible": 1,
"fieldname": "transit_section",
"fieldtype": "Section Break",
"label": "Transit"
},
{
"fieldname": "column_break_qajx",
"fieldtype": "Column Break"
}
],
"icon": "fa fa-building",
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2022-03-01 02:37:48.034944",
"modified": "2023-05-29 13:10:43.333160",
"modified_by": "Administrator",
"module": "Stock",
"name": "Warehouse",
@@ -261,7 +272,6 @@
"read": 1,
"report": 1,
"role": "Item Manager",
"set_user_permissions": 1,
"share": 1,
"write": 1
},

View File

@@ -2,54 +2,75 @@
"creation": "2021-08-24 14:44:46.770952",
"docstatus": 0,
"doctype": "Form Tour",
"first_document": 0,
"idx": 0,
"include_name_field": 0,
"is_standard": 1,
"modified": "2021-08-25 16:26:11.718664",
"list_name": "List",
"modified": "2023-05-29 13:38:27.192177",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation",
"new_document_form": 0,
"owner": "Administrator",
"reference_doctype": "Stock Reconciliation",
"save_on_complete": 1,
"steps": [
{
"description": "Set Purpose to Opening Stock to set the stock opening balance.",
"field": "",
"fieldname": "purpose",
"fieldtype": "Select",
"has_next_condition": 1,
"hide_buttons": 0,
"is_table_field": 0,
"label": "Purpose",
"modal_trigger": 0,
"next_on_click": 0,
"next_step_condition": "eval: doc.purpose === \"Opening Stock\"",
"parent_field": "",
"offset_x": 0,
"offset_y": 0,
"popover_element": 0,
"position": "Top",
"title": "Purpose"
},
{
"description": "Select the items for which the opening stock has to be set.",
"field": "",
"fieldname": "items",
"fieldtype": "Table",
"has_next_condition": 1,
"is_table_field": 0,
"label": "Items",
"next_step_condition": "eval: doc.items[0]?.item_code",
"parent_field": "",
"position": "Top",
"title": "Items"
"title": "Purpose",
"ui_tour": 0
},
{
"description": "Edit the Posting Date by clicking on the Edit Posting Date and Time checkbox below.",
"field": "",
"fieldname": "posting_date",
"fieldtype": "Date",
"has_next_condition": 0,
"hide_buttons": 0,
"is_table_field": 0,
"label": "Posting Date",
"parent_field": "",
"modal_trigger": 0,
"next_on_click": 0,
"offset_x": 0,
"offset_y": 0,
"popover_element": 0,
"position": "Bottom",
"title": "Posting Date"
"title": "Posting Date",
"ui_tour": 0
},
{
"description": "Select the items for which the opening stock has to be set.",
"fieldname": "items",
"fieldtype": "Table",
"has_next_condition": 1,
"hide_buttons": 0,
"is_table_field": 0,
"label": "Items",
"modal_trigger": 0,
"next_on_click": 0,
"next_step_condition": "eval: doc.items[0]?.item_code",
"offset_x": 0,
"offset_y": 0,
"popover_element": 0,
"position": "Top",
"title": "Items",
"ui_tour": 0
}
],
"title": "Stock Reconciliation"
"title": "Stock Reconciliation",
"track_steps": 0,
"ui_tour": 0
}

View File

@@ -2,88 +2,73 @@
"creation": "2021-08-20 15:20:59.336585",
"docstatus": 0,
"doctype": "Form Tour",
"first_document": 0,
"idx": 0,
"include_name_field": 0,
"is_standard": 1,
"modified": "2021-08-25 16:19:37.699528",
"list_name": "List",
"modified": "2023-05-29 12:33:19.142202",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",
"new_document_form": 0,
"owner": "Administrator",
"reference_doctype": "Stock Settings",
"save_on_complete": 1,
"steps": [
{
"description": "By default, the Item Name is set as per the Item Code entered. If you want Items to be named by a Naming Series choose the 'Naming Series' option.",
"field": "",
"fieldname": "item_naming_by",
"fieldtype": "Select",
"has_next_condition": 0,
"hide_buttons": 0,
"is_table_field": 0,
"label": "Item Naming By",
"parent_field": "",
"modal_trigger": 0,
"next_on_click": 0,
"offset_x": 0,
"offset_y": 0,
"popover_element": 0,
"position": "Bottom",
"title": "Item Naming By"
"title": "Item Naming By",
"ui_tour": 0
},
{
"description": "Set a Default Warehouse for Inventory Transactions. This will be fetched into the Default Warehouse in the Item master.",
"field": "",
"fieldname": "default_warehouse",
"fieldtype": "Link",
"has_next_condition": 0,
"hide_buttons": 0,
"is_table_field": 0,
"label": "Default Warehouse",
"parent_field": "",
"modal_trigger": 0,
"next_on_click": 0,
"offset_x": 0,
"offset_y": 0,
"popover_element": 0,
"position": "Bottom",
"title": "Default Warehouse"
},
{
"description": "Quality inspection is performed on the inward and outward movement of goods. Receipt and delivery transactions will be stopped or the user will be warned if the quality inspection is not performed.",
"field": "",
"fieldname": "action_if_quality_inspection_is_not_submitted",
"fieldtype": "Select",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Action If Quality Inspection Is Not Submitted",
"parent_field": "",
"position": "Bottom",
"title": "Action if Quality Inspection Is Not Submitted"
},
{
"description": "Serial numbers for stock will be set automatically based on the Items entered based on first in first out in transactions like Purchase/Sales Invoices, Delivery Notes, etc.",
"field": "",
"fieldname": "automatically_set_serial_nos_based_on_fifo",
"fieldtype": "Check",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Automatically Set Serial Nos Based on FIFO",
"parent_field": "",
"position": "Bottom",
"title": "Automatically Set Serial Nos based on FIFO"
},
{
"description": "Show 'Scan Barcode' field above every child table to insert Items with ease.",
"field": "",
"fieldname": "show_barcode_field",
"fieldtype": "Check",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Show Barcode Field in Stock Transactions",
"parent_field": "",
"position": "Bottom",
"title": "Show Barcode Field"
"title": "Default Warehouse",
"ui_tour": 0
},
{
"description": "Choose between FIFO and Moving Average Valuation Methods. Click <a href=\"https://docs.erpnext.com/docs/user/manual/en/stock/articles/item-valuation-fifo-and-moving-average\" target=\"_blank\">here</a> to know more about them.",
"field": "",
"fieldname": "valuation_method",
"fieldtype": "Select",
"has_next_condition": 0,
"hide_buttons": 0,
"is_table_field": 0,
"label": "Default Valuation Method",
"parent_field": "",
"modal_trigger": 0,
"next_on_click": 0,
"offset_x": 0,
"offset_y": 0,
"popover_element": 0,
"position": "Bottom",
"title": "Default Valuation Method"
"title": "Default Valuation Method",
"ui_tour": 0
}
],
"title": "Stock Settings"
"title": "Stock Settings",
"track_steps": 0,
"ui_tour": 0
}

View File

@@ -2,53 +2,57 @@
"creation": "2021-08-24 14:43:44.465237",
"docstatus": 0,
"doctype": "Form Tour",
"first_document": 0,
"idx": 0,
"include_name_field": 0,
"is_standard": 1,
"modified": "2021-08-24 14:50:31.988256",
"list_name": "List",
"modified": "2023-05-29 13:09:49.920796",
"modified_by": "Administrator",
"module": "Stock",
"name": "Warehouse",
"new_document_form": 0,
"owner": "Administrator",
"reference_doctype": "Warehouse",
"save_on_complete": 1,
"steps": [
{
"description": "Select a name for the warehouse. This should reflect its location or purpose.",
"field": "",
"fieldname": "warehouse_name",
"fieldtype": "Data",
"has_next_condition": 1,
"hide_buttons": 0,
"is_table_field": 0,
"label": "Warehouse Name",
"modal_trigger": 0,
"next_on_click": 0,
"next_step_condition": "eval: doc.warehouse_name",
"parent_field": "",
"offset_x": 0,
"offset_y": 0,
"popover_element": 0,
"position": "Bottom",
"title": "Warehouse Name"
},
{
"description": "Select a warehouse type to categorize the warehouse into a sub-group.",
"field": "",
"fieldname": "warehouse_type",
"fieldtype": "Link",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Warehouse Type",
"parent_field": "",
"position": "Top",
"title": "Warehouse Type"
"title": "Warehouse Name",
"ui_tour": 0
},
{
"description": "Select an account to set a default account for all transactions with this warehouse.",
"field": "",
"fieldname": "account",
"fieldtype": "Link",
"has_next_condition": 0,
"hide_buttons": 0,
"is_table_field": 0,
"label": "Account",
"parent_field": "",
"modal_trigger": 0,
"next_on_click": 0,
"offset_x": 0,
"offset_y": 0,
"popover_element": 0,
"position": "Top",
"title": "Account"
"title": "Account",
"ui_tour": 0
}
],
"title": "Warehouse"
"title": "Warehouse",
"track_steps": 0,
"ui_tour": 0
}

View File

@@ -19,7 +19,7 @@
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/stock",
"idx": 0,
"is_complete": 0,
"modified": "2021-08-20 14:38:55.570067",
"modified": "2023-05-29 14:43:36.223302",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock",
@@ -35,10 +35,10 @@
"step": "Create a Stock Entry"
},
{
"step": "Stock Opening Balance"
"step": "Check Stock Ledger Report"
},
{
"step": "View Stock Projected Qty"
"step": "Stock Opening Balance"
}
],
"subtitle": "Inventory, Warehouses, Analysis, and more.",

View File

@@ -0,0 +1,24 @@
{
"action": "View Report",
"action_label": "Check Stock Ledger",
"creation": "2023-05-29 13:46:04.174565",
"description": "# Check Stock Reports\nBased on the various stock transactions, you can get a host of one-click Stock Reports in ERPNext like Stock Ledger, Stock Balance, Projected Quantity, and Ageing analysis.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2023-05-29 14:39:03.943244",
"modified_by": "Administrator",
"name": "Check Stock Ledger Report",
"owner": "Administrator",
"reference_report": "Stock Ledger",
"report_description": "Stock Ledger report contains every submitted stock transaction. You can use filter to narrow down ledger entries.",
"report_reference_doctype": "Stock Ledger Entry",
"report_type": "Script Report",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Check Stock Ledger",
"validate_action": 1
}

View File

@@ -9,7 +9,7 @@
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-06-18 13:57:11.434063",
"modified": "2023-05-29 14:39:04.066547",
"modified_by": "Administrator",
"name": "Create a Stock Entry",
"owner": "Administrator",

View File

@@ -9,7 +9,7 @@
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-08-18 12:23:36.675572",
"modified": "2023-05-29 14:39:04.074907",
"modified_by": "Administrator",
"name": "Create a Warehouse",
"owner": "Administrator",

View File

@@ -9,7 +9,7 @@
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-06-18 13:59:36.021097",
"modified": "2023-05-29 14:39:08.825699",
"modified_by": "Administrator",
"name": "Stock Opening Balance",
"owner": "Administrator",

View File

@@ -9,7 +9,7 @@
"is_complete": 0,
"is_single": 1,
"is_skipped": 0,
"modified": "2021-08-18 12:06:51.139387",
"modified": "2023-05-29 14:39:04.083360",
"modified_by": "Administrator",
"name": "Stock Settings",
"owner": "Administrator",

View File

@@ -281,7 +281,7 @@ class FIFOSlots:
# consume transfer data and add stock to fifo queue
self.__adjust_incoming_transfer_qty(transfer_data, fifo_queue, row)
else:
if not serial_nos:
if not serial_nos and not row.get("has_serial_no"):
if fifo_queue and flt(fifo_queue[0][0]) <= 0:
# neutralize 0/negative stock by adding positive stock
fifo_queue[0][0] += flt(row.actual_qty)

View File

@@ -5,15 +5,13 @@ from typing import List
import frappe
from frappe import _, scrub
from frappe.query_builder.functions import CombineDatetime
from frappe.utils import get_first_day as get_first_day_of_month
from frappe.utils import get_first_day_of_week, get_quarter_start, getdate
from frappe.utils.nestedset import get_descendants_of
from erpnext.accounts.utils import get_fiscal_year
from erpnext.stock.report.stock_balance.stock_balance import (
get_item_details,
get_items,
get_stock_ledger_entries,
)
from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter
from erpnext.stock.utils import is_reposting_item_valuation_in_progress
@@ -231,7 +229,7 @@ def get_data(filters):
data = []
items = get_items(filters)
sle = get_stock_ledger_entries(filters, items)
item_details = get_item_details(items, sle, filters)
item_details = get_item_details(items, sle)
periodic_data = get_periodic_data(sle, filters)
ranges = get_period_date_ranges(filters)
@@ -265,3 +263,109 @@ def get_chart_data(columns):
chart["type"] = "line"
return chart
def get_items(filters):
"Get items based on item code, item group or brand."
if item_code := filters.get("item_code"):
return [item_code]
else:
item_filters = {}
if item_group := filters.get("item_group"):
children = get_descendants_of("Item Group", item_group, ignore_permissions=True)
item_filters["item_group"] = ("in", children + [item_group])
if brand := filters.get("brand"):
item_filters["brand"] = brand
return frappe.get_all("Item", filters=item_filters, pluck="name", order_by=None)
def get_stock_ledger_entries(filters, items):
sle = frappe.qb.DocType("Stock Ledger Entry")
query = (
frappe.qb.from_(sle)
.select(
sle.item_code,
sle.warehouse,
sle.posting_date,
sle.actual_qty,
sle.valuation_rate,
sle.company,
sle.voucher_type,
sle.qty_after_transaction,
sle.stock_value_difference,
sle.item_code.as_("name"),
sle.voucher_no,
sle.stock_value,
sle.batch_no,
)
.where((sle.docstatus < 2) & (sle.is_cancelled == 0))
.orderby(CombineDatetime(sle.posting_date, sle.posting_time))
.orderby(sle.creation)
.orderby(sle.actual_qty)
)
if items:
query = query.where(sle.item_code.isin(items))
query = apply_conditions(query, filters)
return query.run(as_dict=True)
def apply_conditions(query, filters):
sle = frappe.qb.DocType("Stock Ledger Entry")
warehouse_table = frappe.qb.DocType("Warehouse")
if not filters.get("from_date"):
frappe.throw(_("'From Date' is required"))
if to_date := filters.get("to_date"):
query = query.where(sle.posting_date <= to_date)
else:
frappe.throw(_("'To Date' is required"))
if company := filters.get("company"):
query = query.where(sle.company == company)
if filters.get("warehouse"):
query = apply_warehouse_filter(query, sle, filters)
elif warehouse_type := filters.get("warehouse_type"):
query = (
query.join(warehouse_table)
.on(warehouse_table.name == sle.warehouse)
.where(warehouse_table.warehouse_type == warehouse_type)
)
return query
def get_item_details(items, sle):
item_details = {}
if not items:
items = list(set(d.item_code for d in sle))
if not items:
return item_details
item_table = frappe.qb.DocType("Item")
query = (
frappe.qb.from_(item_table)
.select(
item_table.name,
item_table.item_name,
item_table.description,
item_table.item_group,
item_table.brand,
item_table.stock_uom,
)
.where(item_table.name.isin(items))
)
result = query.run(as_dict=1)
for item_table in result:
item_details.setdefault(item_table.name, item_table)
return item_details

View File

@@ -87,6 +87,12 @@ frappe.query_reports["Stock Balance"] = {
"label": __('Show Stock Ageing Data'),
"fieldtype": 'Check'
},
{
"fieldname": 'ignore_closing_balance',
"label": __('Ignore Closing Balance'),
"fieldtype": 'Check',
"default": 1
},
],
"formatter": function (value, row, column, data, default_formatter) {

File diff suppressed because it is too large Load Diff

View File

@@ -8,15 +8,15 @@
import frappe
from frappe import _
from frappe.query_builder.functions import Count
from frappe.utils import flt
from frappe.utils import cint, flt, getdate
from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, get_average_age
from erpnext.stock.report.stock_balance.stock_balance import (
from erpnext.stock.report.stock_analytics.stock_analytics import (
get_item_details,
get_item_warehouse_map,
get_items,
get_stock_ledger_entries,
)
from erpnext.stock.report.stock_balance.stock_balance import filter_items_with_no_transactions
from erpnext.stock.utils import is_reposting_item_valuation_in_progress
@@ -32,7 +32,7 @@ def execute(filters=None):
items = get_items(filters)
sle = get_stock_ledger_entries(filters, items)
item_map = get_item_details(items, sle, filters)
item_map = get_item_details(items, sle)
iwb_map = get_item_warehouse_map(filters, sle)
warehouse_list = get_warehouse_list(filters)
item_ageing = FIFOSlots(filters).generate()
@@ -128,3 +128,59 @@ def add_warehouse_column(columns, warehouse_list):
for wh in warehouse_list:
columns += [_(wh.name) + ":Int:100"]
def get_item_warehouse_map(filters, sle):
iwb_map = {}
from_date = getdate(filters.get("from_date"))
to_date = getdate(filters.get("to_date"))
float_precision = cint(frappe.db.get_default("float_precision")) or 3
for d in sle:
group_by_key = get_group_by_key(d)
if group_by_key not in iwb_map:
iwb_map[group_by_key] = frappe._dict(
{
"opening_qty": 0.0,
"opening_val": 0.0,
"in_qty": 0.0,
"in_val": 0.0,
"out_qty": 0.0,
"out_val": 0.0,
"bal_qty": 0.0,
"bal_val": 0.0,
"val_rate": 0.0,
}
)
qty_dict = iwb_map[group_by_key]
if d.voucher_type == "Stock Reconciliation" and not d.batch_no:
qty_diff = flt(d.qty_after_transaction) - flt(qty_dict.bal_qty)
else:
qty_diff = flt(d.actual_qty)
value_diff = flt(d.stock_value_difference)
if d.posting_date < from_date:
qty_dict.opening_qty += qty_diff
qty_dict.opening_val += value_diff
elif d.posting_date >= from_date and d.posting_date <= to_date:
if flt(qty_diff, float_precision) >= 0:
qty_dict.in_qty += qty_diff
qty_dict.in_val += value_diff
else:
qty_dict.out_qty += abs(qty_diff)
qty_dict.out_val += abs(value_diff)
qty_dict.val_rate = d.valuation_rate
qty_dict.bal_qty += qty_diff
qty_dict.bal_val += value_diff
iwb_map = filter_items_with_no_transactions(iwb_map, float_precision)
return iwb_map
def get_group_by_key(row) -> tuple:
return (row.company, row.item_code, row.warehouse)

View File

@@ -443,12 +443,11 @@ class update_entries_after(object):
i += 1
self.process_sle(sle)
self.update_bin_data(sle)
if sle.dependant_sle_voucher_detail_no:
entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
self.update_bin()
if self.exceptions:
self.raise_exceptions()
@@ -1065,6 +1064,18 @@ class update_entries_after(object):
else:
raise NegativeStockError(message)
def update_bin_data(self, sle):
bin_name = get_or_make_bin(sle.item_code, sle.warehouse)
values_to_update = {
"actual_qty": sle.qty_after_transaction,
"stock_value": sle.stock_value,
}
if sle.valuation_rate is not None:
values_to_update["valuation_rate"] = sle.valuation_rate
frappe.db.set_value("Bin", bin_name, values_to_update)
def update_bin(self):
# update bin for each warehouse
for warehouse, data in self.data.items():

View File

@@ -220,7 +220,7 @@ def get_bin(item_code, warehouse):
def get_or_make_bin(item_code: str, warehouse: str) -> str:
bin_record = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse})
bin_record = frappe.get_cached_value("Bin", {"item_code": item_code, "warehouse": warehouse})
if not bin_record:
bin_obj = _create_bin(item_code, warehouse)