Merge pull request #41666 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
ruthra kumar
2024-05-29 13:22:52 +05:30
committed by GitHub
46 changed files with 937 additions and 188 deletions

View File

@@ -59,12 +59,14 @@ repos:
rev: v0.2.0
hooks:
- id: ruff
name: "Run ruff linter and apply fixes"
args: ["--fix"]
name: "Run ruff import sorter"
args: ["--select=I", "--fix"]
- id: ruff
name: "Run ruff linter"
- id: ruff-format
name: "Format Python code"
name: "Run ruff formatter"
ci:
autoupdate_schedule: weekly

View File

@@ -70,7 +70,7 @@ class POSClosingEntry(StatusUpdater):
for key, value in pos_occurences.items():
if len(value) > 1:
error_list.append(
_(f"{frappe.bold(key)} is added multiple times on rows: {frappe.bold(value)}")
_("{0} is added multiple times on rows: {1}").format(frappe.bold(key), frappe.bold(value))
)
if error_list:

View File

@@ -228,6 +228,7 @@ class POSInvoice(SalesInvoice):
self.apply_loyalty_points()
self.check_phone_payments()
self.set_status(update=True)
self.make_bundle_for_sales_purchase_return()
self.submit_serial_batch_bundle()
if self.coupon_code:

View File

@@ -318,29 +318,28 @@ class TestPOSInvoice(unittest.TestCase):
pos.insert()
pos.submit()
pos.reload()
pos_return1 = make_sales_return(pos.name)
# partial return 1
pos_return1.get("items")[0].qty = -1
pos_return1.submit()
pos_return1.reload()
bundle_id = frappe.get_doc(
"Serial and Batch Bundle", pos_return1.get("items")[0].serial_and_batch_bundle
)
bundle_id.remove(bundle_id.entries[1])
bundle_id.save()
bundle_id.load_from_db()
serial_no = bundle_id.entries[0].serial_no
self.assertEqual(serial_no, serial_nos[0])
pos_return1.insert()
pos_return1.submit()
# partial return 2
pos_return2 = make_sales_return(pos.name)
pos_return2.submit()
self.assertEqual(pos_return2.get("items")[0].qty, -1)
serial_no = get_serial_nos_from_bundle(pos_return2.get("items")[0].serial_and_batch_bundle)[0]
self.assertEqual(serial_no, serial_nos[1])

View File

@@ -54,7 +54,7 @@ class POSInvoiceMergeLog(Document):
for key, value in pos_occurences.items():
if len(value) > 1:
error_list.append(
_(f"{frappe.bold(key)} is added multiple times on rows: {frappe.bold(value)}")
_("{0} is added multiple times on rows: {1}").format(frappe.bold(key), frappe.bold(value))
)
if error_list:
@@ -481,7 +481,7 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
if closing_entry:
closing_entry.set_status(update=True, status="Failed")
if isinstance(error_message, list):
error_message = frappe.json.dumps(error_message)
error_message = json.dumps(error_message)
closing_entry.db_set("error_message", error_message)
raise

View File

@@ -1,6 +1,8 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import json
import frappe
from frappe import _, qb
from frappe.model.document import Document
@@ -504,7 +506,7 @@ def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
running_doc = None
if for_filter:
if isinstance(for_filter, str):
for_filter = frappe.json.loads(for_filter)
for_filter = json.loads(for_filter)
running_doc = frappe.db.get_value(
"Process Payment Reconciliation",

View File

@@ -448,7 +448,7 @@ class PurchaseInvoice(BuyingController):
stock_not_billed_account = self.get_company_default("stock_received_but_not_billed")
stock_items = self.get_stock_items()
asset_received_but_not_billed = None
asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
if self.update_stock:
self.validate_item_code()
@@ -531,26 +531,40 @@ class PurchaseInvoice(BuyingController):
frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = stock_not_billed_account
elif item.is_fixed_asset and item.pr_detail:
if not asset_received_but_not_billed:
asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
item.expense_account = asset_received_but_not_billed
elif item.is_fixed_asset:
account_type = (
"capital_work_in_progress_account"
if is_cwip_accounting_enabled(item.asset_category)
else "fixed_asset_account"
)
asset_category_account = get_asset_category_account(
account_type, item=item.item_code, company=self.company
)
if not asset_category_account:
form_link = get_link_to_form("Asset Category", item.asset_category)
throw(
_("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company),
title=_("Missing Account"),
account = None
if item.pr_detail:
# check if 'Asset Received But Not Billed' account is credited in Purchase receipt or not
arbnb_booked_in_pr = frappe.db.get_value(
"GL Entry",
{
"voucher_type": "Purchase Receipt",
"voucher_no": item.purchase_receipt,
"account": asset_received_but_not_billed,
},
"name",
)
item.expense_account = asset_category_account
if arbnb_booked_in_pr:
account = asset_received_but_not_billed
if not account:
account_type = (
"capital_work_in_progress_account"
if is_cwip_accounting_enabled(item.asset_category)
else "fixed_asset_account"
)
account = get_asset_category_account(
account_type, item=item.item_code, company=self.company
)
if not account:
form_link = get_link_to_form("Asset Category", item.asset_category)
throw(
_("Please set Fixed Asset Account in {} against {}.").format(
form_link, self.company
),
title=_("Missing Account"),
)
item.expense_account = account
elif not item.expense_account and for_validate:
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
@@ -707,6 +721,7 @@ class PurchaseInvoice(BuyingController):
# Updating stock ledger should always be called after updating prevdoc status,
# because updating ordered qty in bin depends upon updated ordered qty in PO
if self.update_stock == 1:
self.make_bundle_for_sales_purchase_return()
self.make_bundle_using_old_serial_batch_fields()
self.update_stock_ledger()

View File

@@ -1,7 +1,5 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format:ACC-REPOST-{#####}",
"creation": "2023-07-04 13:07:32.923675",
"default_view": "List",
"doctype": "DocType",
@@ -55,11 +53,10 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-09-26 14:21:27.362567",
"modified": "2024-05-23 17:00:42.984798",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Accounting Ledger",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{

View File

@@ -1,6 +1,5 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2022-10-19 21:59:33.553852",
"doctype": "DocType",
"editable_grid": 1,
@@ -99,7 +98,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-09-26 14:21:35.719727",
"modified": "2024-05-23 17:00:31.540640",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Payment Ledger",

View File

@@ -450,6 +450,7 @@ class SalesInvoice(SellingController):
if not self.get(table_name):
continue
self.make_bundle_for_sales_purchase_return(table_name)
self.make_bundle_using_old_serial_batch_fields(table_name)
self.update_stock_ledger()

View File

@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt
import copy
import json
import frappe
from frappe.model.dynamic_links import get_dynamic_link_map
@@ -3720,9 +3721,9 @@ class TestSalesInvoice(FrappeTestCase):
map_docs(
method="erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
source_names=frappe.json.dumps([dn1.name, dn2.name]),
source_names=json.dumps([dn1.name, dn2.name]),
target_doc=si,
args=frappe.json.dumps({"customer": dn1.customer, "merge_taxes": 1, "filtered_children": []}),
args=json.dumps({"customer": dn1.customer, "merge_taxes": 1, "filtered_children": []}),
)
si.save().submit()

View File

@@ -870,7 +870,8 @@
"label": "Purchase Order",
"options": "Purchase Order",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "column_break_92",
@@ -926,7 +927,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-02-25 15:56:44.828634",
"modified": "2024-05-23 16:36:18.970862",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
@@ -936,4 +937,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -1,6 +1,8 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import json
import frappe
from frappe import _, qb
from frappe.model.document import Document
@@ -161,7 +163,7 @@ def get_linked_payments_for_doc(
@frappe.whitelist()
def create_unreconcile_doc_for_selection(selections=None):
if selections:
selections = frappe.json.loads(selections)
selections = json.loads(selections)
# assuming each row is a unique voucher
for row in selections:
unrecon = frappe.new_doc("Unreconcile Payment")

View File

@@ -3,7 +3,9 @@
import frappe
from frappe import _
from frappe import _, qb
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Abs
from frappe.utils import flt, getdate
from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport
@@ -21,16 +23,12 @@ def execute(filters=None):
data = []
for d in entries:
invoice = invoice_details.get(d.against_voucher) or frappe._dict()
if d.reference_type == "Purchase Invoice":
payment_amount = flt(d.debit) or -1 * flt(d.credit)
else:
payment_amount = flt(d.credit) or -1 * flt(d.debit)
invoice = invoice_details.get(d.against_voucher_no) or frappe._dict()
payment_amount = d.amount
d.update({"range1": 0, "range2": 0, "range3": 0, "range4": 0, "outstanding": payment_amount})
if d.against_voucher:
if d.against_voucher_no:
ReceivablePayableReport(filters).get_ageing_data(invoice.posting_date, d)
row = [
@@ -39,11 +37,10 @@ def execute(filters=None):
d.party_type,
d.party,
d.posting_date,
d.against_voucher,
d.against_voucher_no,
invoice.posting_date,
invoice.due_date,
d.debit,
d.credit,
d.amount,
d.remarks,
d.age,
d.range1,
@@ -111,8 +108,7 @@ def get_columns(filters):
"width": 100,
},
{"fieldname": "due_date", "label": _("Payment Due Date"), "fieldtype": "Date", "width": 100},
{"fieldname": "debit", "label": _("Debit"), "fieldtype": "Currency", "width": 140},
{"fieldname": "credit", "label": _("Credit"), "fieldtype": "Currency", "width": 140},
{"fieldname": "amount", "label": _("Amount"), "fieldtype": "Currency", "width": 140},
{"fieldname": "remarks", "label": _("Remarks"), "fieldtype": "Data", "width": 200},
{"fieldname": "age", "label": _("Age"), "fieldtype": "Int", "width": 50},
{"fieldname": "range1", "label": _("0-30"), "fieldtype": "Currency", "width": 140},
@@ -129,51 +125,68 @@ def get_columns(filters):
def get_conditions(filters):
ple = qb.DocType("Payment Ledger Entry")
conditions = []
if not filters.party_type:
if filters.payment_type == _("Outgoing"):
filters.party_type = "Supplier"
else:
filters.party_type = "Customer"
if filters.party_type:
conditions.append("party_type=%(party_type)s")
conditions.append(ple.delinked.eq(0))
if filters.payment_type == _("Outgoing"):
conditions.append(ple.party_type.eq("Supplier"))
conditions.append(ple.against_voucher_type.eq("Purchase Invoice"))
else:
conditions.append(ple.party_type.eq("Customer"))
conditions.append(ple.against_voucher_type.eq("Sales Invoice"))
if filters.party:
conditions.append("party=%(party)s")
if filters.party_type:
conditions.append("against_voucher_type=%(reference_type)s")
filters["reference_type"] = (
"Sales Invoice" if filters.party_type == "Customer" else "Purchase Invoice"
)
conditions.append(ple.party.eq(filters.party))
if filters.get("from_date"):
conditions.append("posting_date >= %(from_date)s")
conditions.append(ple.posting_date.gte(filters.get("from_date")))
if filters.get("to_date"):
conditions.append("posting_date <= %(to_date)s")
conditions.append(ple.posting_date.lte(filters.get("to_date")))
return "and " + " and ".join(conditions) if conditions else ""
if filters.get("company"):
conditions.append(ple.company.eq(filters.get("company")))
return conditions
def get_entries(filters):
return frappe.db.sql(
"""select
voucher_type, voucher_no, party_type, party, posting_date, debit, credit, remarks, against_voucher
from `tabGL Entry`
where company=%(company)s and voucher_type in ('Journal Entry', 'Payment Entry') and is_cancelled = 0 {}
""".format(get_conditions(filters)),
filters,
as_dict=1,
ple = qb.DocType("Payment Ledger Entry")
conditions = get_conditions(filters)
query = (
qb.from_(ple)
.select(
ple.voucher_type,
ple.voucher_no,
ple.party_type,
ple.party,
ple.posting_date,
Abs(ple.amount).as_("amount"),
ple.remarks,
ple.against_voucher_no,
)
.where(Criterion.all(conditions))
)
res = query.run(as_dict=True)
return res
def get_invoice_posting_date_map(filters):
invoice_details = {}
dt = "Sales Invoice" if filters.get("payment_type") == _("Incoming") else "Purchase Invoice"
for t in frappe.db.sql(f"select name, posting_date, due_date from `tab{dt}`", as_dict=1):
dt = (
qb.DocType("Sales Invoice")
if filters.get("payment_type") == _("Incoming")
else qb.DocType("Purchase Invoice")
)
res = (
qb.from_(dt)
.select(dt.name, dt.posting_date, dt.due_date)
.where((dt.docstatus.eq(1)) & (dt.company.eq(filters.get("company"))))
.run(as_dict=1)
)
for t in res:
invoice_details[t.name] = t
return invoice_details

View File

@@ -56,7 +56,7 @@ def get_fiscal_year(
date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False, boolean=False
):
if isinstance(boolean, str):
boolean = frappe.json.loads(boolean)
boolean = loads(boolean)
fiscal_years = get_fiscal_years(
date, fiscal_year, label, verbose, company, as_dict=as_dict, boolean=boolean

View File

@@ -513,7 +513,7 @@ erpnext.buying.RequestforQuotationController = class RequestforQuotationControll
method: "frappe.desk.doctype.tag.tag.get_tagged_docs",
args: {
doctype: "Supplier",
tag: args.tag,
tag: "%" + args.tag + "%",
},
callback: load_suppliers,
});

View File

@@ -1,11 +1,12 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from collections import defaultdict
import frappe
from frappe import _
from frappe.model.meta import get_field_precision
from frappe.utils import flt, format_datetime, get_datetime
from frappe.utils import cint, flt, format_datetime, get_datetime
import erpnext
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
@@ -513,6 +514,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
target_doc.rejected_warehouse = ""
target_doc.warehouse = source_doc.rejected_warehouse
target_doc.received_qty = target_doc.qty
target_doc.return_qty_from_rejected_warehouse = 1
elif doctype == "Purchase Invoice":
returned_qty_map = get_returned_qty_map_for_row(
@@ -570,7 +572,14 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return
if source_doc.item_code:
if (
(source_doc.serial_no or source_doc.batch_no)
and not source_doc.serial_and_batch_bundle
and not source_doc.use_serial_batch_fields
):
target_doc.set("use_serial_batch_fields", 1)
if source_doc.item_code and target_doc.get("use_serial_batch_fields"):
item_details = frappe.get_cached_value(
"Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
)
@@ -578,14 +587,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
if not item_details.has_batch_no and not item_details.has_serial_no:
return
if not target_doc.get("use_serial_batch_fields"):
for qty_field in ["stock_qty", "rejected_qty"]:
if not target_doc.get(qty_field):
continue
update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field)
elif target_doc.get("use_serial_batch_fields"):
update_non_bundled_serial_nos(source_doc, target_doc, source_parent)
update_non_bundled_serial_nos(source_doc, target_doc, source_parent)
def update_non_bundled_serial_nos(source_doc, target_doc, source_parent):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -839,3 +841,229 @@ def get_returned_batches(child_doc, parent_doc, batch_no_field=None, ignore_vouc
batches.update(get_batches_from_bundle(ids))
return batches
def available_serial_batch_for_return(field, doctype, reference_ids, is_rejected=False):
available_dict = get_available_serial_batches(field, doctype, reference_ids, is_rejected=is_rejected)
if not available_dict:
frappe.throw(_("No Serial / Batches are available for return"))
return available_dict
def get_available_serial_batches(field, doctype, reference_ids, is_rejected=False):
_bundle_ids = get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=is_rejected)
if not _bundle_ids:
return frappe._dict({})
return get_serial_batches_based_on_bundle(field, _bundle_ids)
def get_serial_batches_based_on_bundle(field, _bundle_ids):
available_dict = frappe._dict({})
batch_serial_nos = frappe.get_all(
"Serial and Batch Bundle",
fields=[
"`tabSerial and Batch Entry`.`serial_no`",
"`tabSerial and Batch Entry`.`batch_no`",
"`tabSerial and Batch Entry`.`qty`",
"`tabSerial and Batch Bundle`.`voucher_detail_no`",
"`tabSerial and Batch Bundle`.`voucher_type`",
"`tabSerial and Batch Bundle`.`voucher_no`",
],
filters=[
["Serial and Batch Bundle", "name", "in", _bundle_ids],
["Serial and Batch Entry", "docstatus", "=", 1],
],
order_by="`tabSerial and Batch Bundle`.`creation`, `tabSerial and Batch Entry`.`idx`",
)
for row in batch_serial_nos:
key = row.voucher_detail_no
if frappe.get_cached_value(row.voucher_type, row.voucher_no, "is_return"):
key = frappe.get_cached_value(row.voucher_type + " Item", row.voucher_detail_no, field)
if key not in available_dict:
available_dict[key] = frappe._dict(
{"qty": 0.0, "serial_nos": defaultdict(float), "batches": defaultdict(float)}
)
available_dict[key]["qty"] += row.qty
if row.serial_no:
available_dict[key]["serial_nos"][row.serial_no] += row.qty
elif row.batch_no:
available_dict[key]["batches"][row.batch_no] += row.qty
return available_dict
def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False):
filters = {"docstatus": 1, "name": ("in", reference_ids), "serial_and_batch_bundle": ("is", "set")}
pluck_field = "serial_and_batch_bundle"
if is_rejected:
del filters["serial_and_batch_bundle"]
filters["rejected_serial_and_batch_bundle"] = ("is", "set")
pluck_field = "rejected_serial_and_batch_bundle"
_bundle_ids = frappe.get_all(
doctype,
filters=filters,
pluck=pluck_field,
)
if not _bundle_ids:
return {}
del filters["name"]
filters[field] = ("in", reference_ids)
if not is_rejected:
_bundle_ids.extend(
frappe.get_all(
doctype,
filters=filters,
pluck="serial_and_batch_bundle",
)
)
else:
fields = [
"serial_and_batch_bundle",
]
if is_rejected:
fields.extend(["rejected_serial_and_batch_bundle", "return_qty_from_rejected_warehouse"])
data = frappe.get_all(
doctype,
fields=fields,
filters=filters,
)
for d in data:
if is_rejected:
if d.get("return_qty_from_rejected_warehouse"):
_bundle_ids.append(d.get("serial_and_batch_bundle"))
else:
_bundle_ids.append(d.get("rejected_serial_and_batch_bundle"))
else:
_bundle_ids.append(d.get("serial_and_batch_bundle"))
return _bundle_ids
def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field=None):
if not qty_field:
qty_field = "qty"
if not warehouse_field:
warehouse_field = "warehouse"
warehouse = row.get(warehouse_field)
qty = abs(row.get(qty_field))
filterd_serial_batch = frappe._dict({"serial_nos": [], "batches": defaultdict(float)})
if data.serial_nos:
available_serial_nos = []
for serial_no, sn_qty in data.serial_nos.items():
if sn_qty != 0:
available_serial_nos.append(serial_no)
if available_serial_nos:
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
available_serial_nos = get_available_serial_nos(available_serial_nos)
if len(available_serial_nos) > qty:
filterd_serial_batch["serial_nos"] = sorted(available_serial_nos[0 : cint(qty)])
else:
filterd_serial_batch["serial_nos"] = available_serial_nos
elif data.batches:
for batch_no, batch_qty in data.batches.items():
if parent_doc.get("is_internal_customer"):
batch_qty = batch_qty * -1
if batch_qty <= 0:
continue
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
batch_qty = get_available_batch_qty(
parent_doc,
batch_no,
warehouse,
)
if batch_qty <= 0:
frappe.throw(
_("Batch {0} is not available in warehouse {1}").format(batch_no, warehouse),
title=_("Batch Not Available for Return"),
)
if qty <= 0:
break
if batch_qty > qty:
filterd_serial_batch["batches"][batch_no] = qty
qty = 0
else:
filterd_serial_batch["batches"][batch_no] += batch_qty
qty -= batch_qty
return filterd_serial_batch
def get_available_batch_qty(parent_doc, batch_no, warehouse):
from erpnext.stock.doctype.batch.batch import get_batch_qty
return get_batch_qty(
batch_no,
warehouse,
posting_date=parent_doc.posting_date,
posting_time=parent_doc.posting_time,
for_stock_levels=True,
)
def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_field=None):
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
type_of_transaction = "Outward"
if parent_doc.doctype in ["Sales Invoice", "Delivery Note", "POS Invoice"]:
type_of_transaction = "Inward"
if not warehouse_field:
warehouse_field = "warehouse"
warehouse = child_doc.get(warehouse_field)
if parent_doc.get("is_internal_customer"):
warehouse = child_doc.get("target_warehouse")
type_of_transaction = "Outward"
cls_obj = SerialBatchCreation(
{
"type_of_transaction": type_of_transaction,
"item_code": child_doc.item_code,
"warehouse": warehouse,
"serial_nos": data.get("serial_nos"),
"batches": data.get("batches"),
"posting_date": parent_doc.posting_date,
"posting_time": parent_doc.posting_time,
"voucher_type": parent_doc.doctype,
"voucher_no": parent_doc.name,
"voucher_detail_no": child_doc.name,
"qty": child_doc.qty,
"company": parent_doc.company,
"do_not_submit": True,
}
).make_serial_and_batch_bundle()
return cls_obj.name
def get_available_serial_nos(serial_nos, warehouse):
return frappe.get_all(
"Serial No", filters={"warehouse": warehouse, "name": ("in", serial_nos)}, pluck="name"
)

View File

@@ -16,6 +16,11 @@ from erpnext.accounts.general_ledger import (
)
from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_fiscal_year
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.controllers.sales_and_purchase_return import (
available_serial_batch_for_return,
filter_serial_batches,
make_serial_batch_bundle_for_return,
)
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
get_evaluated_inventory_dimension,
@@ -217,6 +222,125 @@ class StockController(AccountsController):
self.update_bundle_details(bundle_details, table_name, row, is_rejected=True)
self.create_serial_batch_bundle(bundle_details, row)
def make_bundle_for_sales_purchase_return(self, table_name=None):
if not self.get("is_return"):
return
if not table_name:
table_name = "items"
self.make_bundle_for_non_rejected_qty(table_name)
if self.doctype in ["Purchase Invoice", "Purchase Receipt"]:
self.make_bundle_for_rejected_qty(table_name)
def make_bundle_for_rejected_qty(self, table_name=None):
field, reference_ids = self.get_reference_ids(
table_name, "rejected_qty", "rejected_serial_and_batch_bundle"
)
if not reference_ids:
return
child_doctype = self.doctype + " Item"
available_dict = available_serial_batch_for_return(
field, child_doctype, reference_ids, is_rejected=True
)
for row in self.get(table_name):
if data := available_dict.get(row.get(field)):
qty_field = "rejected_qty"
warehouse_field = "rejected_warehouse"
if row.get("return_qty_from_rejected_warehouse"):
qty_field = "qty"
warehouse_field = "warehouse"
data = filter_serial_batches(
self, data, row, warehouse_field=warehouse_field, qty_field=qty_field
)
bundle = make_serial_batch_bundle_for_return(data, row, self, warehouse_field)
if row.get("return_qty_from_rejected_warehouse"):
row.db_set(
{
"serial_and_batch_bundle": bundle,
"batch_no": "",
"serial_no": "",
}
)
else:
row.db_set(
{
"rejected_serial_and_batch_bundle": bundle,
"batch_no": "",
"rejected_serial_no": "",
}
)
def make_bundle_for_non_rejected_qty(self, table_name):
field, reference_ids = self.get_reference_ids(table_name)
if not reference_ids:
return
child_doctype = self.doctype + " Item"
available_dict = available_serial_batch_for_return(field, child_doctype, reference_ids)
for row in self.get(table_name):
if data := available_dict.get(row.get(field)):
data = filter_serial_batches(self, data, row)
bundle = make_serial_batch_bundle_for_return(data, row, self)
row.db_set(
{
"serial_and_batch_bundle": bundle,
"batch_no": "",
"serial_no": "",
}
)
def get_reference_ids(self, table_name, qty_field=None, bundle_field=None) -> tuple[str, list[str]]:
field = {
"Sales Invoice": "sales_invoice_item",
"Delivery Note": "dn_detail",
"Purchase Receipt": "purchase_receipt_item",
"Purchase Invoice": "purchase_invoice_item",
"POS Invoice": "pos_invoice_item",
}.get(self.doctype)
if not bundle_field:
bundle_field = "serial_and_batch_bundle"
if not qty_field:
qty_field = "qty"
reference_ids = []
for row in self.get(table_name):
if not self.is_serial_batch_item(row.item_code):
continue
if (
row.get(field)
and (
qty_field == "qty"
and not row.get("return_qty_from_rejected_warehouse")
or qty_field == "rejected_qty"
and (row.get("return_qty_from_rejected_warehouse") or row.get("rejected_warehouse"))
)
and not row.get("use_serial_batch_fields")
and not row.get(bundle_field)
):
reference_ids.append(row.get(field))
return field, reference_ids
@frappe.request_cache
def is_serial_batch_item(self, item_code) -> bool:
item_details = frappe.db.get_value("Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1)
if item_details.has_serial_no or item_details.has_batch_no:
return True
return False
def update_bundle_details(self, bundle_details, table_name, row, is_rejected=False):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -611,35 +735,16 @@ class StockController(AccountsController):
def make_package_for_transfer(
self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None
):
bundle_doc = frappe.get_doc("Serial and Batch Bundle", serial_and_batch_bundle)
if not type_of_transaction:
type_of_transaction = "Inward"
bundle_doc = frappe.copy_doc(bundle_doc)
bundle_doc.warehouse = warehouse
bundle_doc.type_of_transaction = type_of_transaction
bundle_doc.voucher_type = self.doctype
bundle_doc.voucher_no = "" if self.is_new() or self.docstatus == 2 else self.name
bundle_doc.is_cancelled = 0
for row in bundle_doc.entries:
row.is_outward = 0
row.qty = abs(row.qty)
row.stock_value_difference = abs(row.stock_value_difference)
if type_of_transaction == "Outward":
row.qty *= -1
row.stock_value_difference *= row.stock_value_difference
row.is_outward = 1
row.warehouse = warehouse
bundle_doc.calculate_qty_and_amount()
bundle_doc.flags.ignore_permissions = True
bundle_doc.flags.ignore_validate = True
bundle_doc.save(ignore_permissions=True)
return bundle_doc.name
return make_bundle_for_material_transfer(
is_new=self.is_new(),
docstatus=self.docstatus,
voucher_type=self.doctype,
voucher_no=self.name,
serial_and_batch_bundle=serial_and_batch_bundle,
warehouse=warehouse,
type_of_transaction=type_of_transaction,
do_not_submit=do_not_submit,
)
def get_sl_entries(self, d, args):
sl_dict = frappe._dict(
@@ -1557,3 +1662,38 @@ def create_item_wise_repost_entries(
repost_entries.append(repost_entry)
return repost_entries
def make_bundle_for_material_transfer(**kwargs):
if isinstance(kwargs, dict):
kwargs = frappe._dict(kwargs)
bundle_doc = frappe.get_doc("Serial and Batch Bundle", kwargs.serial_and_batch_bundle)
if not kwargs.type_of_transaction:
kwargs.type_of_transaction = "Inward"
bundle_doc = frappe.copy_doc(bundle_doc)
bundle_doc.warehouse = kwargs.warehouse
bundle_doc.type_of_transaction = kwargs.type_of_transaction
bundle_doc.voucher_type = kwargs.voucher_type
bundle_doc.voucher_no = "" if kwargs.is_new or kwargs.docstatus == 2 else kwargs.voucher_no
bundle_doc.is_cancelled = 0
for row in bundle_doc.entries:
row.is_outward = 0
row.qty = abs(row.qty)
row.stock_value_difference = abs(row.stock_value_difference)
if kwargs.type_of_transaction == "Outward":
row.qty *= -1
row.stock_value_difference *= row.stock_value_difference
row.is_outward = 1
row.warehouse = kwargs.warehouse
bundle_doc.calculate_qty_and_amount()
bundle_doc.flags.ignore_permissions = True
bundle_doc.flags.ignore_validate = True
bundle_doc.save(ignore_permissions=True)
return bundle_doc.name

View File

@@ -327,13 +327,13 @@ class SubcontractingController(StockController):
consumed_bundles.batch_nos[batch_no] += abs(qty)
# Will be deprecated in v16
if row.serial_no:
if row.serial_no and not consumed_bundles.serial_nos:
self.available_materials[key]["serial_no"] = list(
set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no))
)
# Will be deprecated in v16
if row.batch_no:
if row.batch_no and not consumed_bundles.batch_nos:
self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty
def get_available_materials(self):

View File

@@ -55,6 +55,14 @@ frappe.ui.form.on("Project", {
filters: filters,
};
});
frm.set_query("cost_center", () => {
return {
filters: {
company: frm.doc.company,
},
};
});
},
refresh: function (frm) {

View File

@@ -83,7 +83,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
this.frm.doc.paid_amount = flt(this.frm.doc.grand_total, precision("grand_total"));
}
this.frm.refresh_fields();
this.frm.refresh_field("taxes");
}
calculate_discount_amount() {
@@ -841,7 +841,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
});
}
this.frm.refresh_fields();
this.frm.refresh_field("taxes");
}
set_default_payment(total_amount_to_pay, update_paid_amount) {

View File

@@ -2,6 +2,8 @@
# License: GNU General Public License v3. See license.txt
import json
import frappe
from frappe.test_runner import make_test_records
from frappe.tests.utils import FrappeTestCase
@@ -321,7 +323,7 @@ class TestCustomer(FrappeTestCase):
frappe.ValidationError,
update_child_qty_rate,
so.doctype,
frappe.json.dumps([modified_item]),
json.dumps([modified_item]),
so.name,
)

View File

@@ -9,5 +9,13 @@ frappe.ui.form.on("Product Bundle", {
query: "erpnext.selling.doctype.product_bundle.product_bundle.get_new_item_code",
};
});
frm.set_query("item_code", "items", () => {
return {
filters: {
has_variants: 0,
},
};
});
},
});

View File

@@ -1139,7 +1139,8 @@
"hide_seconds": 1,
"label": "Inter Company Order Reference",
"options": "Purchase Order",
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "project",
@@ -1645,7 +1646,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2024-03-29 16:27:41.539613",
"modified": "2024-05-23 16:35:54.905804",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",

View File

@@ -8,8 +8,11 @@ from pypika import Order
class DeprecatedSerialNoValuation:
@deprecated
def calculate_stock_value_from_deprecarated_ledgers(self):
if not frappe.db.get_value(
"Stock Ledger Entry", {"serial_no": ("is", "set"), "is_cancelled": 0}, "name"
if not frappe.db.get_all(
"Stock Ledger Entry",
fields=["name"],
filters={"serial_no": ("is", "set"), "is_cancelled": 0, "item_code": self.sle.item_code},
limit=1,
):
return
@@ -41,6 +44,12 @@ class DeprecatedSerialNoValuation:
# get rate from serial nos within same company
incoming_values = 0.0
for serial_no in serial_nos:
sn_details = frappe.db.get_value("Serial No", serial_no, ["purchase_rate", "company"], as_dict=1)
if sn_details and sn_details.purchase_rate and sn_details.company == self.sle.company:
self.serial_no_incoming_rate[serial_no] += flt(sn_details.purchase_rate)
incoming_values += self.serial_no_incoming_rate[serial_no]
continue
table = frappe.qb.DocType("Stock Ledger Entry")
stock_ledgers = (
frappe.qb.from_(table)

View File

@@ -208,7 +208,8 @@ def get_batch_qty(
:param batch_no: Optional - give qty for this batch no
:param warehouse: Optional - give qty for this warehouse
:param item_code: Optional - give qty for this item"""
:param item_code: Optional - give qty for this item
:param for_stock_levels: True consider expired batches"""
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
get_auto_batch_nos,

View File

@@ -467,6 +467,7 @@ class DeliveryNote(SellingController):
if not self.get(table_name):
continue
self.make_bundle_for_sales_purchase_return(table_name)
self.make_bundle_using_old_serial_batch_fields(table_name)
# Updating stock ledger should always be called after updating prevdoc status,
@@ -1371,6 +1372,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
if source_parent.doctype == "Delivery Note" and source.received_qty:
target.qty = flt(source.qty) + flt(source.returned_qty) - flt(source.received_qty)
if source.get("use_serial_batch_fields"):
target.set("use_serial_batch_fields", 1)
doclist = get_mapped_doc(
doctype,
source_name,

View File

@@ -249,18 +249,15 @@ class TestDeliveryNote(FrappeTestCase):
self.assertTrue(dn.items[0].serial_no)
frappe.flags.ignore_serial_batch_bundle_validation = False
frappe.flags.use_serial_and_batch_fields = False
# return entry
dn1 = make_sales_return(dn.name)
dn1.items[0].qty = -2
bundle_doc = frappe.get_doc("Serial and Batch Bundle", dn1.items[0].serial_and_batch_bundle)
bundle_doc.set("entries", bundle_doc.entries[:2])
bundle_doc.save()
dn1.save()
dn1.items[0].serial_no = "\n".join(get_serial_nos(serial_nos)[0:2])
dn1.submit()
dn1.reload()
returned_serial_nos1 = get_serial_nos_from_bundle(dn1.items[0].serial_and_batch_bundle)
for serial_no in returned_serial_nos1:
@@ -269,21 +266,15 @@ class TestDeliveryNote(FrappeTestCase):
dn2 = make_sales_return(dn.name)
dn2.items[0].qty = -2
bundle_doc = frappe.get_doc("Serial and Batch Bundle", dn2.items[0].serial_and_batch_bundle)
bundle_doc.set("entries", bundle_doc.entries[:2])
bundle_doc.save()
dn2.save()
dn2.items[0].serial_no = "\n".join(get_serial_nos(serial_nos)[2:4])
dn2.submit()
dn2.reload()
returned_serial_nos2 = get_serial_nos_from_bundle(dn2.items[0].serial_and_batch_bundle)
for serial_no in returned_serial_nos2:
self.assertTrue(serial_no in serial_nos)
self.assertFalse(serial_no in returned_serial_nos1)
frappe.flags.use_serial_and_batch_fields = False
def test_sales_return_for_non_bundled_items_partial(self):
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
@@ -428,7 +419,7 @@ class TestDeliveryNote(FrappeTestCase):
self.assertEqual(dn.per_returned, 100)
self.assertEqual(dn.status, "Return Issued")
def test_delivery_note_return_valuation_on_different_warehuose(self):
def test_delivery_note_return_valuation_on_different_warehouse(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")

View File

@@ -370,6 +370,7 @@ class PurchaseReceipt(BuyingController):
else:
self.db_set("status", "Completed")
self.make_bundle_for_sales_purchase_return()
self.make_bundle_using_old_serial_batch_fields()
# Updating stock ledger should always be called after updating prevdoc status,
# because updating ordered qty, reserved_qty_for_subcontract in bin

View File

@@ -2645,7 +2645,7 @@ class TestPurchaseReceipt(FrappeTestCase):
for row in inter_transfer_dn_return.items:
self.assertTrue(row.serial_and_batch_bundle)
def test_internal_transfer_with_serial_batch_items_without_user_serial_batch_fields(self):
def test_internal_transfer_with_serial_batch_items_without_use_serial_batch_fields(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note

View File

@@ -81,6 +81,7 @@
"purchase_invoice",
"column_break_40",
"allow_zero_valuation_rate",
"return_qty_from_rejected_warehouse",
"is_fixed_asset",
"asset_location",
"asset_category",
@@ -1116,12 +1117,19 @@
"hidden": 1,
"label": "Apply TDS",
"read_only": 1
},
{
"default": "0",
"fieldname": "return_qty_from_rejected_warehouse",
"fieldtype": "Check",
"label": "Return Qty from Rejected Warehouse",
"read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-04-08 20:00:16.278292",
"modified": "2024-05-28 09:48:24.448815",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
@@ -1132,4 +1140,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -85,6 +85,7 @@ class PurchaseReceiptItem(Document):
rejected_serial_no: DF.Text | None
rejected_warehouse: DF.Link | None
retain_sample: DF.Check
return_qty_from_rejected_warehouse: DF.Check
returned_qty: DF.Float
rm_supp_cost: DF.Currency
sales_order: DF.Link | None

View File

@@ -377,7 +377,7 @@ class TestPutawayRule(FrappeTestCase):
apply_putaway_rule=1,
do_not_save=1,
)
stock_entry.save()
stock_entry.submit()
stock_entry.load_from_db()
self.assertEqual(stock_entry.items[0].t_warehouse, self.warehouse_1)
@@ -398,11 +398,17 @@ class TestPutawayRule(FrappeTestCase):
self.assertUnchangedItemsOnResave(stock_entry)
for row in stock_entry.items:
if row.serial_and_batch_bundle:
frappe.delete_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
stock_entry.load_from_db()
stock_entry.cancel()
rivs = frappe.get_all("Repost Item Valuation", filters={"voucher_no": stock_entry.name})
for row in rivs:
riv_doc = frappe.get_doc("Repost Item Valuation", row.name)
riv_doc.cancel()
riv_doc.delete()
frappe.db.set_single_value("Accounts Settings", "delete_linked_ledger_entries", 1)
stock_entry.delete()
pr.cancel()
rule_1.delete()

View File

@@ -156,6 +156,8 @@ class SerialandBatchBundle(Document):
def validate_serial_nos_duplicate(self):
# Don't inward same serial number multiple times
if self.voucher_type in ["POS Invoice", "Pick List"]:
return
if not self.warehouse:
return

View File

@@ -111,6 +111,8 @@ frappe.ui.form.on("Stock Entry", {
// or a pre-existing batch
if (frm.doc.purpose != "Material Receipt") {
filters["warehouse"] = item.s_warehouse || item.t_warehouse;
} else {
filters["is_inward"] = 1;
}
return {
@@ -1110,6 +1112,13 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
on_submit() {
this.clean_up();
this.refresh_serial_batch_bundle_field();
}
refresh_serial_batch_bundle_field() {
frappe.route_hooks.after_submit = (frm_obj) => {
frm_obj.reload_doc();
};
}
after_cancel() {

View File

@@ -226,6 +226,7 @@ class StockEntry(StockController):
if not self.from_bom:
self.fg_completed_qty = 0.0
self.make_serial_and_batch_bundle_for_outward()
self.validate_serialized_batch()
self.calculate_rate_and_amount()
self.validate_putaway_capacity()
@@ -289,9 +290,6 @@ class StockEntry(StockController):
if self.purpose == "Material Transfer" and self.outgoing_stock_entry:
self.set_material_request_transfer_status("In Transit")
def before_save(self):
self.make_serial_and_batch_bundle_for_outward()
def on_update(self):
self.set_serial_and_batch_bundle()
@@ -992,7 +990,7 @@ class StockEntry(StockController):
self.purpose = frappe.get_cached_value("Stock Entry Type", self.stock_entry_type, "purpose")
def make_serial_and_batch_bundle_for_outward(self):
if self.docstatus == 1:
if self.docstatus == 0:
return
serial_or_batch_items = get_serial_or_batch_items(self.items)
@@ -1045,12 +1043,11 @@ class StockEntry(StockController):
if not bundle_doc:
continue
if self.docstatus == 0:
for entry in bundle_doc.entries:
if not entry.serial_no:
continue
for entry in bundle_doc.entries:
if not entry.serial_no:
continue
already_picked_serial_nos.append(entry.serial_no)
already_picked_serial_nos.append(entry.serial_no)
row.serial_and_batch_bundle = bundle_doc.name

View File

@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe import _, throw
from frappe.contacts.address_and_contact import load_address_and_contact

View File

@@ -4,7 +4,7 @@
import frappe
from frappe import _
from frappe.utils import cint, flt, get_table_name, getdate
from frappe.utils import add_to_date, cint, flt, get_datetime, get_table_name, getdate
from frappe.utils.deprecations import deprecated
from pypika import functions as fn
@@ -107,6 +107,8 @@ def get_stock_ledger_entries_for_batch_no(filters):
if not filters.get("to_date"):
frappe.throw(_("'To Date' is required"))
posting_datetime = get_datetime(add_to_date(filters["to_date"], days=1))
sle = frappe.qb.DocType("Stock Ledger Entry")
query = (
frappe.qb.from_(sle)
@@ -121,7 +123,7 @@ def get_stock_ledger_entries_for_batch_no(filters):
(sle.docstatus < 2)
& (sle.is_cancelled == 0)
& (sle.batch_no != "")
& (sle.posting_date <= filters["to_date"])
& (sle.posting_datetime < posting_datetime)
)
.groupby(sle.voucher_no, sle.batch_no, sle.item_code, sle.warehouse)
.orderby(sle.item_code, sle.warehouse)

View File

@@ -9,6 +9,9 @@ from frappe import _
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos as get_serial_nos_from_sle
from erpnext.stock.stock_ledger import get_stock_ledger_entries
BUYING_VOUCHER_TYPES = ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]
SELLING_VOUCHER_TYPES = ["Sales Invoice", "Delivery Note"]
def execute(filters=None):
columns = get_columns(filters)
@@ -72,6 +75,20 @@ def get_columns(filters):
"fieldname": "qty",
"width": 150,
},
{
"label": _("Party Type"),
"fieldtype": "Link",
"fieldname": "party_type",
"options": "DocType",
"width": 90,
},
{
"label": _("Party"),
"fieldtype": "Dynamic Link",
"fieldname": "party",
"options": "party_type",
"width": 120,
},
]
return columns
@@ -102,6 +119,17 @@ def get_data(filters):
}
)
# get party details depending on the voucher type
party_field = (
"supplier"
if row.voucher_type in BUYING_VOUCHER_TYPES
else ("customer" if row.voucher_type in SELLING_VOUCHER_TYPES else None)
)
args.party_type = party_field.title() if party_field else None
args.party = (
frappe.db.get_value(row.voucher_type, row.voucher_no, party_field) if party_field else None
)
serial_nos = []
if row.serial_no:
parsed_serial_nos = get_serial_nos_from_sle(row.serial_no)

View File

@@ -27,7 +27,11 @@ def execute(filters=None):
items = get_items(filters)
sl_entries = get_stock_ledger_entries(filters, items)
item_details = get_item_details(items, sl_entries, include_uom)
opening_row = get_opening_balance(filters, columns, sl_entries)
if filters.get("batch_no"):
opening_row = get_opening_balance_from_batch(filters, columns, sl_entries)
else:
opening_row = get_opening_balance(filters, columns, sl_entries)
precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
bundle_details = {}
@@ -48,13 +52,16 @@ def execute(filters=None):
available_serial_nos = {}
inventory_dimension_filters_applied = check_inventory_dimension_filters_applied(filters)
batch_balance_dict = defaultdict(float)
batch_balance_dict = frappe._dict({})
if actual_qty and filters.get("batch_no"):
batch_balance_dict[filters.batch_no] = [actual_qty, stock_value]
for sle in sl_entries:
item_detail = item_details[sle.item_code]
sle.update(item_detail)
if bundle_info := bundle_details.get(sle.serial_and_batch_bundle):
data.extend(get_segregated_bundle_entries(sle, bundle_info, batch_balance_dict))
data.extend(get_segregated_bundle_entries(sle, bundle_info, batch_balance_dict, filters))
continue
if filters.get("batch_no") or inventory_dimension_filters_applied:
@@ -90,7 +97,7 @@ def execute(filters=None):
return columns, data
def get_segregated_bundle_entries(sle, bundle_details, batch_balance_dict):
def get_segregated_bundle_entries(sle, bundle_details, batch_balance_dict, filters):
segregated_entries = []
qty_before_transaction = sle.qty_after_transaction - sle.actual_qty
stock_value_before_transaction = sle.stock_value - sle.stock_value_difference
@@ -109,9 +116,19 @@ def get_segregated_bundle_entries(sle, bundle_details, batch_balance_dict):
}
)
if row.batch_no:
batch_balance_dict[row.batch_no] += row.qty
new_sle.update({"qty_after_transaction": batch_balance_dict[row.batch_no]})
if filters.get("batch_no") and row.batch_no:
if not batch_balance_dict.get(row.batch_no):
batch_balance_dict[row.batch_no] = [0, 0]
batch_balance_dict[row.batch_no][0] += row.qty
batch_balance_dict[row.batch_no][1] += row.stock_value_difference
new_sle.update(
{
"qty_after_transaction": batch_balance_dict[row.batch_no][0],
"stock_value": batch_balance_dict[row.batch_no][1],
}
)
qty_before_transaction += row.qty
stock_value_before_transaction += new_sle.stock_value_difference
@@ -504,6 +521,62 @@ def get_sle_conditions(filters):
return "and {}".format(" and ".join(conditions)) if conditions else ""
def get_opening_balance_from_batch(filters, columns, sl_entries):
query_filters = {
"batch_no": filters.batch_no,
"docstatus": 1,
"posting_date": ("<", filters.from_date),
}
for fields in ["item_code", "warehouse"]:
if filters.get(fields):
query_filters[fields] = filters.get(fields)
opening_data = frappe.get_all(
"Stock Ledger Entry",
fields=["sum(actual_qty) as qty_after_transaction", "sum(stock_value_difference) as stock_value"],
filters=query_filters,
)[0]
for field in ["qty_after_transaction", "stock_value", "valuation_rate"]:
if opening_data.get(field) is None:
opening_data[field] = 0.0
query_filters = [
["Serial and Batch Entry", "batch_no", "=", filters.batch_no],
["Serial and Batch Bundle", "docstatus", "=", 1],
["Serial and Batch Bundle", "posting_date", "<", filters.from_date],
]
for fields in ["item_code", "warehouse"]:
if filters.get(fields):
query_filters.append(["Serial and Batch Bundle", fields, "=", filters.get(fields)])
bundle_data = frappe.get_all(
"Serial and Batch Bundle",
fields=[
"sum(`tabSerial and Batch Entry`.`qty`) as qty",
"sum(`tabSerial and Batch Entry`.`stock_value_difference`) as stock_value",
],
filters=query_filters,
)
if bundle_data:
opening_data.qty_after_transaction += flt(bundle_data[0].qty)
opening_data.stock_value += flt(bundle_data[0].stock_value)
if opening_data.qty_after_transaction:
opening_data.valuation_rate = flt(opening_data.stock_value) / flt(
opening_data.qty_after_transaction
)
return {
"item_code": _("'Opening'"),
"qty_after_transaction": opening_data.qty_after_transaction,
"valuation_rate": opening_data.valuation_rate,
"stock_value": opening_data.stock_value,
}
def get_opening_balance(filters, columns, sl_entries):
if not (filters.item_code and filters.warehouse and filters.from_date):
return

View File

@@ -55,8 +55,45 @@ class SerialBatchBundle:
elif not self.sle.is_cancelled:
self.validate_item_and_warehouse()
def is_material_transfer(self):
allowed_types = [
"Material Transfer",
"Send to Subcontractor",
"Material Transfer for Manufacture",
]
if (
self.sle.voucher_type == "Stock Entry"
and not self.sle.is_cancelled
and frappe.get_cached_value("Stock Entry", self.sle.voucher_no, "purpose") in allowed_types
):
return True
def make_serial_batch_no_bundle_for_material_transfer(self):
from erpnext.controllers.stock_controller import make_bundle_for_material_transfer
bundle = frappe.db.get_value(
"Stock Entry Detail", self.sle.voucher_detail_no, "serial_and_batch_bundle"
)
if bundle:
new_bundle_id = make_bundle_for_material_transfer(
is_new=False,
docstatus=1,
voucher_type=self.sle.voucher_type,
voucher_no=self.sle.voucher_no,
serial_and_batch_bundle=bundle,
warehouse=self.sle.warehouse,
type_of_transaction="Inward" if self.sle.actual_qty > 0 else "Outward",
do_not_submit=0,
)
self.sle.db_set({"serial_and_batch_bundle": new_bundle_id})
def make_serial_batch_no_bundle(self):
self.validate_item()
if self.sle.actual_qty > 0 and self.is_material_transfer():
self.make_serial_batch_no_bundle_for_material_transfer()
return
sn_doc = SerialBatchCreation(
{
@@ -143,6 +180,9 @@ class SerialBatchBundle:
"serial_and_batch_bundle": sn_doc.name,
}
if self.sle.actual_qty < 0 and self.is_material_transfer():
values_to_update["valuation_rate"] = sn_doc.avg_rate
if not frappe.db.get_single_value(
"Stock Settings", "do_not_update_serial_batch_on_creation_of_auto_bundle"
):
@@ -341,11 +381,9 @@ def get_serial_nos(serial_and_batch_bundle, serial_nos=None):
if serial_nos:
filters["serial_no"] = ("in", serial_nos)
entries = frappe.get_all("Serial and Batch Entry", fields=["serial_no"], filters=filters, order_by="idx")
if not entries:
return []
serial_nos = frappe.get_all("Serial and Batch Entry", filters=filters, order_by="idx", pluck="serial_no")
return [d.serial_no for d in entries if d.serial_no]
return serial_nos
def get_batches_from_bundle(serial_and_batch_bundle, batches=None):

View File

@@ -308,7 +308,15 @@ def get_reposting_data(file_path) -> dict:
attached_file = frappe.get_doc("File", file_name)
data = gzip.decompress(attached_file.get_content())
content = attached_file.get_content()
if isinstance(content, str):
content = content.encode("utf-8")
try:
data = gzip.decompress(content)
except Exception:
return frappe._dict()
if data := json.loads(data.decode("utf-8")):
data = data
@@ -1428,7 +1436,11 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc
order by posting_datetime desc, creation desc
limit 1
for update""",
args,
{
"item_code": args.get("item_code"),
"warehouse": args.get("warehouse"),
"posting_datetime": args.get("posting_datetime"),
},
as_dict=1,
)

View File

@@ -302,6 +302,21 @@ frappe.ui.form.on("Subcontracting Receipt", {
};
}
},
reset_raw_materials_table: (frm) => {
frm.clear_table("supplied_items");
frm.call({
method: "reset_raw_materials",
doc: frm.doc,
freeze: true,
callback: (r) => {
if (!r.exc) {
frm.save();
}
},
});
},
});
frappe.ui.form.on("Landed Cost Taxes and Charges", {

View File

@@ -47,8 +47,11 @@
"total_qty",
"column_break_27",
"total",
"raw_material_details",
"raw_materials_consumed_section",
"reset_raw_materials_table",
"column_break_uinr",
"get_current_stock",
"raw_material_details",
"supplied_items",
"additional_costs_section",
"distribute_additional_costs_based_on",
@@ -300,6 +303,7 @@
"depends_on": "supplied_items",
"fieldname": "raw_material_details",
"fieldtype": "Section Break",
"hide_border": 1,
"label": "Raw Materials Consumed",
"options": "fa fa-table",
"print_hide": 1,
@@ -640,12 +644,26 @@
"fieldname": "supplier_delivery_note",
"fieldtype": "Data",
"label": "Supplier Delivery Note"
},
{
"fieldname": "raw_materials_consumed_section",
"fieldtype": "Section Break",
"label": "Raw Materials Actions"
},
{
"fieldname": "reset_raw_materials_table",
"fieldtype": "Button",
"label": "Reset Raw Materials Table"
},
{
"fieldname": "column_break_uinr",
"fieldtype": "Column Break"
}
],
"in_create": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-11-16 13:04:00.710534",
"modified": "2024-05-28 15:02:13.517969",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Receipt",
@@ -714,4 +732,4 @@
"timeline_field": "supplier",
"title_field": "title",
"track_changes": 1
}
}

View File

@@ -179,6 +179,11 @@ class SubcontractingReceipt(SubcontractingController):
self.update_status()
self.delete_auto_created_batches()
@frappe.whitelist()
def reset_raw_materials(self):
self.supplied_items = []
self.create_raw_materials_supplied()
def validate_closed_subcontracting_order(self):
for item in self.items:
if item.subcontracting_order:

View File

@@ -1235,6 +1235,116 @@ class TestSubcontractingReceipt(FrappeTestCase):
self.assertTrue(scr.items[0].serial_and_batch_bundle)
self.assertTrue(scr.items[0].rejected_serial_and_batch_bundle)
def test_subcontracting_receipt_for_batch_materials_without_use_serial_batch_fields(self):
from erpnext.controllers.subcontracting_controller import make_rm_stock_entry
set_backflush_based_on("Material Transferred for Subcontract")
fg_item = make_item(
"Test Subcontracted FG Item With Batch No and Without Use Serial Batch Fields",
properties={"is_stock_item": 1, "is_sub_contracted_item": 1},
).name
rm_item1 = make_item(
"Test Subcontracted RM Item With Batch No and Without Use Serial Batch Fields",
properties={
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "BATCH-RM-BNGS-.####",
},
).name
make_item(
"Subcontracted Service Item 21",
properties={
"is_stock_item": 0,
},
)
bom = make_bom(item=fg_item, raw_materials=[rm_item1])
rm_batch_no = None
for row in bom.items:
se = make_stock_entry(
item_code=row.item_code,
qty=10,
target="_Test Warehouse - _TC",
rate=300,
)
se.reload()
rm_batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 21",
"qty": 10,
"rate": 100,
"fg_item": fg_item,
"fg_item_qty": 10,
},
]
sco = get_subcontracting_order(service_items=service_items)
self.assertTrue(sco.docstatus)
rm_items = [
{
"name": sco.supplied_items[0].name,
"item_code": rm_item1,
"rm_item_code": rm_item1,
"item_name": rm_item1,
"qty": 10,
"warehouse": "_Test Warehouse - _TC",
"rate": 100,
"stock_uom": frappe.get_cached_value("Item", rm_item1, "stock_uom"),
"use_serial_batch_fields": 1,
},
]
se = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items))
se.items[0].subcontracted_item = fg_item
se.items[0].s_warehouse = "_Test Warehouse - _TC"
se.items[0].t_warehouse = "_Test Warehouse 1 - _TC"
se.items[0].use_serial_batch_fields = 1
se.items[0].batch_no = rm_batch_no
se.submit()
self.assertEqual(se.items[0].batch_no, rm_batch_no)
self.assertEqual(se.items[0].use_serial_batch_fields, 1)
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
scr = make_subcontracting_receipt(sco.name)
scr.items[0].qty = 2
scr.save()
scr.submit()
self.assertEqual(scr.supplied_items[0].consumed_qty, 2)
self.assertEqual(scr.supplied_items[0].batch_no, rm_batch_no)
self.assertEqual(get_batch_from_bundle(scr.supplied_items[0].serial_and_batch_bundle), rm_batch_no)
scr = make_subcontracting_receipt(sco.name)
scr.items[0].qty = 2
scr.save()
scr.submit()
self.assertEqual(scr.supplied_items[0].consumed_qty, 2)
self.assertEqual(scr.supplied_items[0].batch_no, rm_batch_no)
self.assertEqual(get_batch_from_bundle(scr.supplied_items[0].serial_and_batch_bundle), rm_batch_no)
scr = make_subcontracting_receipt(sco.name)
scr.items[0].qty = 6
scr.save()
scr.submit()
self.assertEqual(scr.supplied_items[0].consumed_qty, 6)
self.assertEqual(scr.supplied_items[0].batch_no, rm_batch_no)
self.assertEqual(get_batch_from_bundle(scr.supplied_items[0].serial_and_batch_bundle), rm_batch_no)
sco.reload()
self.assertEqual(sco.status, "Completed")
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
def make_return_subcontracting_receipt(**args):
args = frappe._dict(args)