mirror of
https://github.com/frappe/erpnext.git
synced 2026-02-17 08:35:00 +00:00
Merge pull request #35473 from frappe/version-14-hotfix
chore: release v14
This commit is contained in:
2
.github/stale.yml
vendored
2
.github/stale.yml
vendored
@@ -13,7 +13,7 @@ exemptProjects: true
|
||||
exemptMilestones: true
|
||||
|
||||
pulls:
|
||||
daysUntilStale: 15
|
||||
daysUntilStale: 14
|
||||
daysUntilClose: 3
|
||||
exemptLabels:
|
||||
- hotfix
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"),
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user