Merge pull request #37964 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
commit
44bad3bd4a
@ -89,7 +89,6 @@ class BankTransaction(StatusUpdater):
|
||||
- 0 > a: Error: already over-allocated
|
||||
- clear means: set the latest transaction date as clearance date
|
||||
"""
|
||||
gl_bank_account = frappe.db.get_value("Bank Account", self.bank_account, "account")
|
||||
remaining_amount = self.unallocated_amount
|
||||
for payment_entry in self.payment_entries:
|
||||
if payment_entry.allocated_amount == 0.0:
|
||||
|
@ -154,7 +154,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.events.set_dynamic_labels(frm);
|
||||
frm.events.show_general_ledger(frm);
|
||||
erpnext.accounts.ledger_preview.show_accounting_ledger_preview(frm);
|
||||
if(frm.doc.references.find((elem) => {return elem.exchange_gain_loss != 0})) {
|
||||
if((frm.doc.references) && (frm.doc.references.find((elem) => {return elem.exchange_gain_loss != 0}))) {
|
||||
frm.add_custom_button(__("View Exchange Gain/Loss Journals"), function() {
|
||||
frappe.set_route("List", "Journal Entry", {"voucher_type": "Exchange Gain Or Loss", "reference_name": frm.doc.name});
|
||||
}, __('Actions'));
|
||||
|
@ -65,7 +65,8 @@
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Voucher Type",
|
||||
"options": "DocType"
|
||||
"options": "DocType",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_no",
|
||||
@ -73,14 +74,16 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Voucher No",
|
||||
"options": "voucher_type"
|
||||
"options": "voucher_type",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "against_voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Against Voucher Type",
|
||||
"options": "DocType"
|
||||
"options": "DocType",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "against_voucher_no",
|
||||
@ -88,7 +91,8 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Against Voucher No",
|
||||
"options": "against_voucher_type"
|
||||
"options": "against_voucher_type",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
@ -148,13 +152,14 @@
|
||||
{
|
||||
"fieldname": "voucher_detail_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Voucher Detail No"
|
||||
"label": "Voucher Detail No",
|
||||
"search_index": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-10-30 16:15:00.470283",
|
||||
"modified": "2023-11-03 16:39:58.904113",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Ledger Entry",
|
||||
|
@ -109,6 +109,8 @@ class PaymentReconciliation(Document):
|
||||
"t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1"
|
||||
)
|
||||
|
||||
limit = f"limit {self.payment_limit}" if self.payment_limit else " "
|
||||
|
||||
# nosemgrep
|
||||
journal_entries = frappe.db.sql(
|
||||
"""
|
||||
@ -132,11 +134,13 @@ class PaymentReconciliation(Document):
|
||||
ELSE {bank_account_condition}
|
||||
END)
|
||||
order by t1.posting_date
|
||||
{limit}
|
||||
""".format(
|
||||
**{
|
||||
"dr_or_cr": dr_or_cr,
|
||||
"bank_account_condition": bank_account_condition,
|
||||
"condition": condition,
|
||||
"limit": limit,
|
||||
}
|
||||
),
|
||||
{
|
||||
@ -162,7 +166,7 @@ class PaymentReconciliation(Document):
|
||||
if self.payment_name:
|
||||
conditions.append(doc.name.like(f"%{self.payment_name}%"))
|
||||
|
||||
self.return_invoices = (
|
||||
self.return_invoices_query = (
|
||||
qb.from_(doc)
|
||||
.select(
|
||||
ConstantColumn(voucher_type).as_("voucher_type"),
|
||||
@ -170,8 +174,11 @@ class PaymentReconciliation(Document):
|
||||
doc.return_against,
|
||||
)
|
||||
.where(Criterion.all(conditions))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
if self.payment_limit:
|
||||
self.return_invoices_query = self.return_invoices_query.limit(self.payment_limit)
|
||||
|
||||
self.return_invoices = self.return_invoices_query.run(as_dict=True)
|
||||
|
||||
def get_dr_or_cr_notes(self):
|
||||
|
||||
|
@ -110,7 +110,7 @@
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-21 17:36:26.642617",
|
||||
"modified": "2023-11-02 11:32:12.254018",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Payment Reconciliation Log",
|
||||
@ -125,7 +125,19 @@
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
|
@ -384,7 +384,8 @@
|
||||
"label": "Supplier Invoice No",
|
||||
"oldfieldname": "bill_no",
|
||||
"oldfieldtype": "Data",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_15",
|
||||
@ -407,7 +408,8 @@
|
||||
"no_copy": 1,
|
||||
"options": "Purchase Invoice",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_addresses",
|
||||
@ -1602,7 +1604,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-10-16 16:24:51.886231",
|
||||
"modified": "2023-11-03 15:47:30.319200",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
@ -1665,4 +1667,4 @@
|
||||
"timeline_field": "supplier",
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
@ -1783,9 +1783,14 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
set_advance_flag(company="_Test Company", flag=0, default_account="")
|
||||
|
||||
def test_gl_entries_for_standalone_debit_note(self):
|
||||
make_purchase_invoice(qty=5, rate=500, update_stock=True)
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
returned_inv = make_purchase_invoice(qty=-5, rate=5, update_stock=True, is_return=True)
|
||||
item_code = make_item(properties={"is_stock_item": 1})
|
||||
make_purchase_invoice(item_code=item_code, qty=5, rate=500, update_stock=True)
|
||||
|
||||
returned_inv = make_purchase_invoice(
|
||||
item_code=item_code, qty=-5, rate=5, update_stock=True, is_return=True
|
||||
)
|
||||
|
||||
# override the rate with valuation rate
|
||||
sle = frappe.get_all(
|
||||
@ -1795,7 +1800,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
)[0]
|
||||
|
||||
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
|
||||
self.assertAlmostEqual(returned_inv.items[0].rate, rate)
|
||||
self.assertAlmostEqual(rate, 500)
|
||||
|
||||
def test_payment_allocation_for_payment_terms(self):
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
@ -1898,6 +1903,12 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
disable_dimension()
|
||||
|
||||
def test_repost_accounting_entries(self):
|
||||
# update repost settings
|
||||
settings = frappe.get_doc("Repost Accounting Ledger Settings")
|
||||
if not [x for x in settings.allowed_types if x.document_type == "Purchase Invoice"]:
|
||||
settings.append("allowed_types", {"document_type": "Purchase Invoice", "allowed": True})
|
||||
settings.save()
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
rate=1000,
|
||||
price_list_rate=1000,
|
||||
|
@ -5,9 +5,7 @@ frappe.ui.form.on("Repost Accounting Ledger", {
|
||||
setup: function(frm) {
|
||||
frm.fields_dict['vouchers'].grid.get_field('voucher_type').get_query = function(doc) {
|
||||
return {
|
||||
filters: {
|
||||
name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']],
|
||||
}
|
||||
query: "erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.get_repost_allowed_types"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,9 +10,12 @@ from frappe.utils.data import comma_and
|
||||
class RepostAccountingLedger(Document):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RepostAccountingLedger, self).__init__(*args, **kwargs)
|
||||
self._allowed_types = set(
|
||||
["Purchase Invoice", "Sales Invoice", "Payment Entry", "Journal Entry"]
|
||||
)
|
||||
self._allowed_types = [
|
||||
x.document_type
|
||||
for x in frappe.db.get_all(
|
||||
"Repost Allowed Types", filters={"allowed": True}, fields=["distinct(document_type)"]
|
||||
)
|
||||
]
|
||||
|
||||
def validate(self):
|
||||
self.validate_vouchers()
|
||||
@ -157,7 +160,7 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
doc.docstatus = 1
|
||||
doc.make_gl_entries()
|
||||
|
||||
elif doc.doctype in ["Payment Entry", "Journal Entry"]:
|
||||
elif doc.doctype in ["Payment Entry", "Journal Entry", "Expense Claim"]:
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
doc.make_gl_entries(1)
|
||||
doc.make_gl_entries()
|
||||
@ -186,3 +189,18 @@ def validate_docs_for_deferred_accounting(sales_docs, purchase_docs):
|
||||
frappe.bold(comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue]))
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_repost_allowed_types(doctype, txt, searchfield, start, page_len, filters):
|
||||
filters = {"allowed": True}
|
||||
|
||||
if txt:
|
||||
filters.update({"document_type": ("like", f"%{txt}%")})
|
||||
|
||||
if allowed_types := frappe.db.get_all(
|
||||
"Repost Allowed Types", filters=filters, fields=["distinct(document_type)"], as_list=1
|
||||
):
|
||||
return allowed_types
|
||||
return []
|
||||
|
@ -20,10 +20,18 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.update_repost_settings()
|
||||
|
||||
def teadDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def update_repost_settings(self):
|
||||
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
|
||||
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
|
||||
for x in allowed_types:
|
||||
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
|
||||
repost_settings.save()
|
||||
|
||||
def test_01_basic_functions(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
|
@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Repost Accounting Ledger Settings", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
@ -0,0 +1,46 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2023-11-07 09:57:20.619939",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"allowed_types"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "allowed_types",
|
||||
"fieldtype": "Table",
|
||||
"label": "Allowed Doctypes",
|
||||
"options": "Repost Allowed Types"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-07 14:24:13.321522",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Accounting Ledger Settings",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Administrator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class RepostAccountingLedgerSettings(Document):
|
||||
pass
|
@ -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 TestRepostAccountingLedgerSettings(FrappeTestCase):
|
||||
pass
|
@ -0,0 +1,45 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2023-11-07 09:58:03.595382",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"document_type",
|
||||
"column_break_sfzb",
|
||||
"allowed"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "document_type",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Doctype",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allowed",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Allowed"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_sfzb",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-07 10:01:39.217861",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Allowed Types",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class RepostAllowedTypes(Document):
|
||||
pass
|
@ -26,6 +26,7 @@
|
||||
"is_return",
|
||||
"return_against",
|
||||
"update_billed_amount_in_sales_order",
|
||||
"update_billed_amount_in_delivery_note",
|
||||
"is_debit_note",
|
||||
"amended_from",
|
||||
"accounting_dimensions_section",
|
||||
@ -2153,6 +2154,13 @@
|
||||
"fieldname": "use_company_roundoff_cost_center",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Company default Cost Center for Round off"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.is_return",
|
||||
"fieldname": "update_billed_amount_in_delivery_note",
|
||||
"fieldtype": "Check",
|
||||
"label": "Update Billed Amount in Delivery Note"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@ -2165,7 +2173,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2023-07-25 16:02:18.988799",
|
||||
"modified": "2023-11-03 14:39:38.012346",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
@ -253,6 +253,7 @@ class SalesInvoice(SellingController):
|
||||
|
||||
self.update_status_updater_args()
|
||||
self.update_prevdoc_status()
|
||||
|
||||
self.update_billing_status_in_dn()
|
||||
self.clear_unallocated_mode_of_payments()
|
||||
|
||||
@ -1019,7 +1020,7 @@ class SalesInvoice(SellingController):
|
||||
|
||||
def make_customer_gl_entry(self, gl_entries):
|
||||
# Checked both rounding_adjustment and rounded_total
|
||||
# because rounded_total had value even before introcution of posting GLE based on rounded total
|
||||
# because rounded_total had value even before introduction of posting GLE based on rounded total
|
||||
grand_total = (
|
||||
self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
|
||||
)
|
||||
@ -1267,7 +1268,7 @@ class SalesInvoice(SellingController):
|
||||
if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount:
|
||||
payment_mode.base_amount -= flt(self.change_amount)
|
||||
|
||||
if payment_mode.amount:
|
||||
if payment_mode.base_amount:
|
||||
# POS, make payment entries
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
@ -1429,6 +1430,8 @@ class SalesInvoice(SellingController):
|
||||
)
|
||||
|
||||
def update_billing_status_in_dn(self, update_modified=True):
|
||||
if self.is_return and not self.update_billed_amount_in_delivery_note:
|
||||
return
|
||||
updated_delivery_notes = []
|
||||
for d in self.get("items"):
|
||||
if d.dn_detail:
|
||||
|
@ -143,7 +143,13 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
"fieldname": "show_future_payments",
|
||||
"label": __("Show Future Payments"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "ignore_accounts",
|
||||
"label": __("Group by Voucher"),
|
||||
"fieldtype": "Check",
|
||||
}
|
||||
|
||||
],
|
||||
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
@ -175,4 +181,4 @@ function get_party_type_options() {
|
||||
});
|
||||
});
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
@ -172,7 +172,13 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
"fieldname": "show_remarks",
|
||||
"label": __("Show Remarks"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "ignore_accounts",
|
||||
"label": __("Group by Voucher"),
|
||||
"fieldtype": "Check",
|
||||
}
|
||||
|
||||
],
|
||||
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
@ -205,4 +211,4 @@ function get_party_type_options() {
|
||||
});
|
||||
});
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
@ -116,7 +116,12 @@ class ReceivablePayableReport(object):
|
||||
# build all keys, since we want to exclude vouchers beyond the report date
|
||||
for ple in self.ple_entries:
|
||||
# get the balance object for voucher_type
|
||||
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
|
||||
|
||||
if self.filters.get("ignore_accounts"):
|
||||
key = (ple.voucher_type, ple.voucher_no, ple.party)
|
||||
else:
|
||||
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
|
||||
|
||||
if not key in self.voucher_balance:
|
||||
self.voucher_balance[key] = frappe._dict(
|
||||
voucher_type=ple.voucher_type,
|
||||
@ -183,7 +188,10 @@ class ReceivablePayableReport(object):
|
||||
):
|
||||
return
|
||||
|
||||
key = (ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)
|
||||
if self.filters.get("ignore_accounts"):
|
||||
key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
|
||||
else:
|
||||
key = (ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)
|
||||
|
||||
# If payment is made against credit note
|
||||
# and credit note is made against a Sales Invoice
|
||||
@ -192,13 +200,19 @@ class ReceivablePayableReport(object):
|
||||
if ple.against_voucher_no in self.return_entries:
|
||||
return_against = self.return_entries.get(ple.against_voucher_no)
|
||||
if return_against:
|
||||
key = (ple.account, ple.against_voucher_type, return_against, ple.party)
|
||||
if self.filters.get("ignore_accounts"):
|
||||
key = (ple.against_voucher_type, return_against, ple.party)
|
||||
else:
|
||||
key = (ple.account, ple.against_voucher_type, return_against, ple.party)
|
||||
|
||||
row = self.voucher_balance.get(key)
|
||||
|
||||
if not row:
|
||||
# no invoice, this is an invoice / stand-alone payment / credit note
|
||||
row = self.voucher_balance.get((ple.account, ple.voucher_type, ple.voucher_no, ple.party))
|
||||
if self.filters.get("ignore_accounts"):
|
||||
row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party))
|
||||
else:
|
||||
row = self.voucher_balance.get((ple.account, ple.voucher_type, ple.voucher_no, ple.party))
|
||||
|
||||
row.party_type = ple.party_type
|
||||
return row
|
||||
|
@ -79,7 +79,9 @@ class General_Payment_Ledger_Comparison(object):
|
||||
.select(
|
||||
gle.company,
|
||||
gle.account,
|
||||
gle.voucher_type,
|
||||
gle.voucher_no,
|
||||
gle.party_type,
|
||||
gle.party,
|
||||
outstanding,
|
||||
)
|
||||
@ -89,7 +91,9 @@ class General_Payment_Ledger_Comparison(object):
|
||||
& (gle.account.isin(val.accounts))
|
||||
)
|
||||
.where(Criterion.all(filter_criterion))
|
||||
.groupby(gle.company, gle.account, gle.voucher_no, gle.party)
|
||||
.groupby(
|
||||
gle.company, gle.account, gle.voucher_type, gle.voucher_no, gle.party_type, gle.party
|
||||
)
|
||||
.run()
|
||||
)
|
||||
|
||||
@ -112,7 +116,13 @@ class General_Payment_Ledger_Comparison(object):
|
||||
self.account_types[acc_type].ple = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.company, ple.account, ple.voucher_no, ple.party, Sum(ple.amount).as_("outstanding")
|
||||
ple.company,
|
||||
ple.account,
|
||||
ple.voucher_type,
|
||||
ple.voucher_no,
|
||||
ple.party_type,
|
||||
ple.party,
|
||||
Sum(ple.amount).as_("outstanding"),
|
||||
)
|
||||
.where(
|
||||
(ple.company == self.filters.company)
|
||||
@ -120,7 +130,9 @@ class General_Payment_Ledger_Comparison(object):
|
||||
& (ple.account.isin(val.accounts))
|
||||
)
|
||||
.where(Criterion.all(filter_criterion))
|
||||
.groupby(ple.company, ple.account, ple.voucher_no, ple.party)
|
||||
.groupby(
|
||||
ple.company, ple.account, ple.voucher_type, ple.voucher_no, ple.party_type, ple.party
|
||||
)
|
||||
.run()
|
||||
)
|
||||
|
||||
@ -138,12 +150,12 @@ class General_Payment_Ledger_Comparison(object):
|
||||
self.diff = frappe._dict({})
|
||||
|
||||
for x in self.variation_in_payment_ledger:
|
||||
self.diff[(x[0], x[1], x[2], x[3])] = frappe._dict({"gl_balance": x[4]})
|
||||
self.diff[(x[0], x[1], x[2], x[3], x[4], x[5])] = frappe._dict({"gl_balance": x[6]})
|
||||
|
||||
for x in self.variation_in_general_ledger:
|
||||
self.diff.setdefault((x[0], x[1], x[2], x[3]), frappe._dict({"gl_balance": 0.0})).update(
|
||||
frappe._dict({"pl_balance": x[4]})
|
||||
)
|
||||
self.diff.setdefault(
|
||||
(x[0], x[1], x[2], x[3], x[4], x[5]), frappe._dict({"gl_balance": 0.0})
|
||||
).update(frappe._dict({"pl_balance": x[6]}))
|
||||
|
||||
def generate_data(self):
|
||||
self.data = []
|
||||
@ -151,8 +163,12 @@ class General_Payment_Ledger_Comparison(object):
|
||||
self.data.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"voucher_no": key[2],
|
||||
"party": key[3],
|
||||
"company": key[0],
|
||||
"account": key[1],
|
||||
"voucher_type": key[2],
|
||||
"voucher_no": key[3],
|
||||
"party_type": key[4],
|
||||
"party": key[5],
|
||||
"gl_balance": val.gl_balance,
|
||||
"pl_balance": val.pl_balance,
|
||||
}
|
||||
@ -162,12 +178,52 @@ class General_Payment_Ledger_Comparison(object):
|
||||
def get_columns(self):
|
||||
self.columns = []
|
||||
options = None
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=_("Company"),
|
||||
fieldname="company",
|
||||
fieldtype="Link",
|
||||
options="Company",
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=_("Account"),
|
||||
fieldname="account",
|
||||
fieldtype="Link",
|
||||
options="Account",
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=_("Voucher Type"),
|
||||
fieldname="voucher_type",
|
||||
fieldtype="Link",
|
||||
options="DocType",
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=_("Voucher No"),
|
||||
fieldname="voucher_no",
|
||||
fieldtype="Data",
|
||||
options=options,
|
||||
fieldtype="Dynamic Link",
|
||||
options="voucher_type",
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=_("Party Type"),
|
||||
fieldname="party_type",
|
||||
fieldtype="Link",
|
||||
options="DocType",
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
@ -176,8 +232,8 @@ class General_Payment_Ledger_Comparison(object):
|
||||
dict(
|
||||
label=_("Party"),
|
||||
fieldname="party",
|
||||
fieldtype="Data",
|
||||
options=options,
|
||||
fieldtype="Dynamic Link",
|
||||
options="party_type",
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
|
@ -50,7 +50,11 @@ class TestGeneralAndPaymentLedger(FrappeTestCase, AccountsTestMixin):
|
||||
self.assertEqual(len(data), 1)
|
||||
|
||||
expected = {
|
||||
"company": sinv.company,
|
||||
"account": sinv.debit_to,
|
||||
"voucher_type": sinv.doctype,
|
||||
"voucher_no": sinv.name,
|
||||
"party_type": "Customer",
|
||||
"party": sinv.customer,
|
||||
"gl_balance": sinv.grand_total,
|
||||
"pl_balance": sinv.grand_total - 1,
|
||||
|
@ -193,7 +193,13 @@ frappe.query_reports["General Ledger"] = {
|
||||
"fieldname": "add_values_in_transaction_currency",
|
||||
"label": __("Add Columns in Transaction Currency"),
|
||||
"fieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"fieldname": "show_remarks",
|
||||
"label": __("Show Remarks"),
|
||||
"fieldtype": "Check"
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -163,6 +163,9 @@ def get_gl_entries(filters, accounting_dimensions):
|
||||
select_fields = """, debit, credit, debit_in_account_currency,
|
||||
credit_in_account_currency """
|
||||
|
||||
if filters.get("show_remarks"):
|
||||
select_fields += """,remarks"""
|
||||
|
||||
order_by_statement = "order by posting_date, account, creation"
|
||||
|
||||
if filters.get("include_dimensions"):
|
||||
@ -195,7 +198,7 @@ def get_gl_entries(filters, accounting_dimensions):
|
||||
voucher_type, voucher_no, {dimension_fields}
|
||||
cost_center, project, {transaction_currency_fields}
|
||||
against_voucher_type, against_voucher, account_currency,
|
||||
remarks, against, is_opening, creation {select_fields}
|
||||
against, is_opening, creation {select_fields}
|
||||
from `tabGL Entry`
|
||||
where company=%(company)s {conditions}
|
||||
{order_by_statement}
|
||||
@ -631,8 +634,10 @@ def get_columns(filters):
|
||||
"width": 100,
|
||||
},
|
||||
{"label": _("Supplier Invoice No"), "fieldname": "bill_no", "fieldtype": "Data", "width": 100},
|
||||
{"label": _("Remarks"), "fieldname": "remarks", "width": 400},
|
||||
]
|
||||
)
|
||||
|
||||
if filters.get("show_remarks"):
|
||||
columns.extend([{"label": _("Remarks"), "fieldname": "remarks", "width": 400}])
|
||||
|
||||
return columns
|
||||
|
@ -47,6 +47,7 @@ def get_result(
|
||||
out = []
|
||||
for name, details in gle_map.items():
|
||||
tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0
|
||||
bill_no, bill_date = "", ""
|
||||
tax_withholding_category = tax_category_map.get(name)
|
||||
rate = tax_rate_map.get(tax_withholding_category)
|
||||
|
||||
@ -70,7 +71,10 @@ def get_result(
|
||||
if net_total_map.get(name):
|
||||
if voucher_type == "Journal Entry":
|
||||
# back calcalute total amount from rate and tax_amount
|
||||
total_amount = grand_total = base_total = tax_amount / (rate / 100)
|
||||
if rate:
|
||||
total_amount = grand_total = base_total = tax_amount / (rate / 100)
|
||||
elif voucher_type == "Purchase Invoice":
|
||||
total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get(name)
|
||||
else:
|
||||
total_amount, grand_total, base_total = net_total_map.get(name)
|
||||
else:
|
||||
@ -96,7 +100,7 @@ def get_result(
|
||||
|
||||
row.update(
|
||||
{
|
||||
"section_code": tax_withholding_category,
|
||||
"section_code": tax_withholding_category or "",
|
||||
"entity_type": party_map.get(party, {}).get(party_type),
|
||||
"rate": rate,
|
||||
"total_amount": total_amount,
|
||||
@ -106,10 +110,14 @@ def get_result(
|
||||
"transaction_date": posting_date,
|
||||
"transaction_type": voucher_type,
|
||||
"ref_no": name,
|
||||
"supplier_invoice_no": bill_no,
|
||||
"supplier_invoice_date": bill_date,
|
||||
}
|
||||
)
|
||||
out.append(row)
|
||||
|
||||
out.sort(key=lambda x: x["section_code"])
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@ -157,14 +165,14 @@ def get_gle_map(documents):
|
||||
def get_columns(filters):
|
||||
pan = "pan" if frappe.db.has_column(filters.party_type, "pan") else "tax_id"
|
||||
columns = [
|
||||
{"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 60},
|
||||
{
|
||||
"label": _(filters.get("party_type")),
|
||||
"fieldname": "party",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "party_type",
|
||||
"width": 180,
|
||||
"label": _("Section Code"),
|
||||
"options": "Tax Withholding Category",
|
||||
"fieldname": "section_code",
|
||||
"fieldtype": "Link",
|
||||
"width": 90,
|
||||
},
|
||||
{"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 60},
|
||||
]
|
||||
|
||||
if filters.naming_series == "Naming Series":
|
||||
@ -179,51 +187,60 @@ def get_columns(filters):
|
||||
|
||||
columns.extend(
|
||||
[
|
||||
{
|
||||
"label": _("Date of Transaction"),
|
||||
"fieldname": "transaction_date",
|
||||
"fieldtype": "Date",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Section Code"),
|
||||
"options": "Tax Withholding Category",
|
||||
"fieldname": "section_code",
|
||||
"fieldtype": "Link",
|
||||
"width": 90,
|
||||
},
|
||||
{"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 100},
|
||||
{
|
||||
"label": _("Total Amount"),
|
||||
"fieldname": "total_amount",
|
||||
"fieldtype": "Float",
|
||||
"width": 90,
|
||||
},
|
||||
]
|
||||
)
|
||||
if filters.party_type == "Supplier":
|
||||
columns.extend(
|
||||
[
|
||||
{
|
||||
"label": _("Supplier Invoice No"),
|
||||
"fieldname": "supplier_invoice_no",
|
||||
"fieldtype": "Data",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Supplier Invoice Date"),
|
||||
"fieldname": "supplier_invoice_date",
|
||||
"fieldtype": "Date",
|
||||
"width": 120,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
columns.extend(
|
||||
[
|
||||
{
|
||||
"label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"),
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Percent",
|
||||
"width": 90,
|
||||
"width": 60,
|
||||
},
|
||||
{
|
||||
"label": _("Tax Amount"),
|
||||
"fieldname": "tax_amount",
|
||||
"label": _("Total Amount"),
|
||||
"fieldname": "total_amount",
|
||||
"fieldtype": "Float",
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
"label": _("Grand Total"),
|
||||
"fieldname": "grand_total",
|
||||
"fieldtype": "Float",
|
||||
"width": 90,
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Base Total"),
|
||||
"fieldname": "base_total",
|
||||
"fieldtype": "Float",
|
||||
"width": 90,
|
||||
"width": 120,
|
||||
},
|
||||
{"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 100},
|
||||
{
|
||||
"label": _("Tax Amount"),
|
||||
"fieldname": "tax_amount",
|
||||
"fieldtype": "Float",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Grand Total"),
|
||||
"fieldname": "grand_total",
|
||||
"fieldtype": "Float",
|
||||
"width": 120,
|
||||
},
|
||||
{"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 130},
|
||||
{
|
||||
"label": _("Reference No."),
|
||||
"fieldname": "ref_no",
|
||||
@ -231,6 +248,12 @@ def get_columns(filters):
|
||||
"options": "transaction_type",
|
||||
"width": 180,
|
||||
},
|
||||
{
|
||||
"label": _("Date of Transaction"),
|
||||
"fieldname": "transaction_date",
|
||||
"fieldtype": "Date",
|
||||
"width": 100,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
@ -253,27 +276,7 @@ def get_tds_docs(filters):
|
||||
"Tax Withholding Account", {"company": filters.get("company")}, pluck="account"
|
||||
)
|
||||
|
||||
query_filters = {
|
||||
"account": ("in", tds_accounts),
|
||||
"posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]),
|
||||
"is_cancelled": 0,
|
||||
"against": ("not in", bank_accounts),
|
||||
}
|
||||
|
||||
party = frappe.get_all(filters.get("party_type"), pluck="name")
|
||||
or_filters.update({"against": ("in", party), "voucher_type": "Journal Entry"})
|
||||
|
||||
if filters.get("party"):
|
||||
del query_filters["account"]
|
||||
del query_filters["against"]
|
||||
or_filters = {"against": filters.get("party"), "party": filters.get("party")}
|
||||
|
||||
tds_docs = frappe.get_all(
|
||||
"GL Entry",
|
||||
filters=query_filters,
|
||||
or_filters=or_filters,
|
||||
fields=["voucher_no", "voucher_type", "against", "party"],
|
||||
)
|
||||
tds_docs = get_tds_docs_query(filters, bank_accounts, tds_accounts).run(as_dict=True)
|
||||
|
||||
for d in tds_docs:
|
||||
if d.voucher_type == "Purchase Invoice":
|
||||
@ -309,6 +312,47 @@ def get_tds_docs(filters):
|
||||
)
|
||||
|
||||
|
||||
def get_tds_docs_query(filters, bank_accounts, tds_accounts):
|
||||
if not tds_accounts:
|
||||
frappe.throw(
|
||||
_("No {0} Accounts found for this company.").format(frappe.bold("Tax Withholding")),
|
||||
title="Accounts Missing Error",
|
||||
)
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
query = (
|
||||
frappe.qb.from_(gle)
|
||||
.select("voucher_no", "voucher_type", "against", "party")
|
||||
.where((gle.is_cancelled == 0))
|
||||
)
|
||||
|
||||
if filters.get("from_date"):
|
||||
query = query.where(gle.posting_date >= filters.get("from_date"))
|
||||
if filters.get("to_date"):
|
||||
query = query.where(gle.posting_date <= filters.get("to_date"))
|
||||
|
||||
if bank_accounts:
|
||||
query = query.where(gle.against.notin(bank_accounts))
|
||||
|
||||
if filters.get("party"):
|
||||
party = [filters.get("party")]
|
||||
query = query.where(
|
||||
((gle.account.isin(tds_accounts) & gle.against.isin(party)))
|
||||
| ((gle.voucher_type == "Journal Entry") & (gle.party == filters.get("party")))
|
||||
| gle.party.isin(party)
|
||||
)
|
||||
else:
|
||||
party = frappe.get_all(filters.get("party_type"), pluck="name")
|
||||
query = query.where(
|
||||
((gle.account.isin(tds_accounts) & gle.against.isin(party)))
|
||||
| (
|
||||
(gle.voucher_type == "Journal Entry")
|
||||
& ((gle.party_type == filters.get("party_type")) | (gle.party_type == ""))
|
||||
)
|
||||
| gle.party.isin(party)
|
||||
)
|
||||
return query
|
||||
|
||||
|
||||
def get_journal_entry_party_map(journal_entries):
|
||||
journal_entry_party_map = {}
|
||||
for d in frappe.db.get_all(
|
||||
@ -335,6 +379,8 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
||||
"base_tax_withholding_net_total",
|
||||
"grand_total",
|
||||
"base_total",
|
||||
"bill_no",
|
||||
"bill_date",
|
||||
],
|
||||
"Sales Invoice": ["base_net_total", "grand_total", "base_total"],
|
||||
"Payment Entry": [
|
||||
@ -353,7 +399,13 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
||||
for entry in entries:
|
||||
tax_category_map.update({entry.name: entry.tax_withholding_category})
|
||||
if doctype == "Purchase Invoice":
|
||||
value = [entry.base_tax_withholding_net_total, entry.grand_total, entry.base_total]
|
||||
value = [
|
||||
entry.base_tax_withholding_net_total,
|
||||
entry.grand_total,
|
||||
entry.base_total,
|
||||
entry.bill_no,
|
||||
entry.bill_date,
|
||||
]
|
||||
elif doctype == "Sales Invoice":
|
||||
value = [entry.base_net_total, entry.grand_total, entry.base_total]
|
||||
elif doctype == "Payment Entry":
|
||||
|
@ -818,7 +818,7 @@ def get_item_details(item_code, asset_category, gross_purchase_amount):
|
||||
"depreciation_method": d.depreciation_method,
|
||||
"total_number_of_depreciations": d.total_number_of_depreciations,
|
||||
"frequency_of_depreciation": d.frequency_of_depreciation,
|
||||
"daily_depreciation": d.daily_depreciation,
|
||||
"daily_prorata_based": d.daily_prorata_based,
|
||||
"salvage_value_percentage": d.salvage_value_percentage,
|
||||
"expected_value_after_useful_life": flt(gross_purchase_amount)
|
||||
* flt(d.salvage_value_percentage / 100),
|
||||
|
@ -780,6 +780,15 @@ def get_disposal_account_and_cost_center(company):
|
||||
def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_book=None):
|
||||
asset_doc = frappe.get_doc("Asset", asset)
|
||||
|
||||
if asset_doc.available_for_use_date > getdate(disposal_date):
|
||||
frappe.throw(
|
||||
"Disposal date {0} cannot be before available for use date {1} of the asset.".format(
|
||||
disposal_date, asset_doc.available_for_use_date
|
||||
)
|
||||
)
|
||||
elif asset_doc.available_for_use_date == getdate(disposal_date):
|
||||
return flt(asset_doc.gross_purchase_amount - asset_doc.opening_accumulated_depreciation)
|
||||
|
||||
if not asset_doc.calculate_depreciation:
|
||||
return flt(asset_doc.value_after_depreciation)
|
||||
|
||||
|
@ -755,7 +755,9 @@ class TestDepreciationMethods(AssetSetup):
|
||||
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
def test_schedule_for_straight_line_method_with_daily_depreciation(self):
|
||||
def test_schedule_for_straight_line_method_with_daily_prorata_based(
|
||||
self,
|
||||
):
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2023-01-01",
|
||||
@ -764,7 +766,7 @@ class TestDepreciationMethods(AssetSetup):
|
||||
depreciation_start_date="2023-01-31",
|
||||
total_number_of_depreciations=12,
|
||||
frequency_of_depreciation=1,
|
||||
daily_depreciation=1,
|
||||
daily_prorata_based=1,
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
@ -1760,7 +1762,7 @@ def create_asset(**args):
|
||||
"total_number_of_depreciations": args.total_number_of_depreciations or 5,
|
||||
"expected_value_after_useful_life": args.expected_value_after_useful_life or 0,
|
||||
"depreciation_start_date": args.depreciation_start_date,
|
||||
"daily_depreciation": args.daily_depreciation or 0,
|
||||
"daily_prorata_based": args.daily_prorata_based or 0,
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
||||
"depreciation_method",
|
||||
"total_number_of_depreciations",
|
||||
"rate_of_depreciation",
|
||||
"daily_depreciation",
|
||||
"daily_prorata_based",
|
||||
"column_break_8",
|
||||
"frequency_of_depreciation",
|
||||
"expected_value_after_useful_life",
|
||||
@ -179,9 +179,9 @@
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.depreciation_method == \"Straight Line\" || doc.depreciation_method == \"Manual\"",
|
||||
"fieldname": "daily_depreciation",
|
||||
"fieldname": "daily_prorata_based",
|
||||
"fieldtype": "Check",
|
||||
"label": "Daily Depreciation",
|
||||
"label": "Depreciate based on daily pro-rata",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
@ -189,7 +189,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-10 22:22:09.722968",
|
||||
"modified": "2023-11-03 21:32:15.021796",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Depreciation Schedule",
|
||||
|
@ -153,7 +153,7 @@ class AssetDepreciationSchedule(Document):
|
||||
self.frequency_of_depreciation = row.frequency_of_depreciation
|
||||
self.rate_of_depreciation = row.rate_of_depreciation
|
||||
self.expected_value_after_useful_life = row.expected_value_after_useful_life
|
||||
self.daily_depreciation = row.daily_depreciation
|
||||
self.daily_prorata_based = row.daily_prorata_based
|
||||
self.status = "Draft"
|
||||
|
||||
def make_depr_schedule(
|
||||
@ -573,7 +573,7 @@ def get_straight_line_or_manual_depr_amount(
|
||||
)
|
||||
# if the Depreciation Schedule is being modified after Asset Value Adjustment due to decrease in asset value
|
||||
elif asset.flags.decrease_in_asset_value_due_to_value_adjustment:
|
||||
if row.daily_depreciation:
|
||||
if row.daily_prorata_based:
|
||||
daily_depr_amount = (
|
||||
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
||||
) / date_diff(
|
||||
@ -618,7 +618,7 @@ def get_straight_line_or_manual_depr_amount(
|
||||
) / number_of_pending_depreciations
|
||||
# if the Depreciation Schedule is being prepared for the first time
|
||||
else:
|
||||
if row.daily_depreciation:
|
||||
if row.daily_prorata_based:
|
||||
daily_depr_amount = (
|
||||
flt(asset.gross_purchase_amount)
|
||||
- flt(asset.opening_accumulated_depreciation)
|
||||
|
@ -8,7 +8,7 @@
|
||||
"finance_book",
|
||||
"depreciation_method",
|
||||
"total_number_of_depreciations",
|
||||
"daily_depreciation",
|
||||
"daily_prorata_based",
|
||||
"column_break_5",
|
||||
"frequency_of_depreciation",
|
||||
"depreciation_start_date",
|
||||
@ -86,23 +86,23 @@
|
||||
"fieldtype": "Percent",
|
||||
"label": "Rate of Depreciation"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.depreciation_method == \"Straight Line\" || doc.depreciation_method == \"Manual\"",
|
||||
"fieldname": "daily_depreciation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Daily Depreciation"
|
||||
},
|
||||
{
|
||||
"fieldname": "salvage_value_percentage",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Salvage Value Percentage"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.depreciation_method == \"Straight Line\" || doc.depreciation_method == \"Manual\"",
|
||||
"fieldname": "daily_prorata_based",
|
||||
"fieldtype": "Check",
|
||||
"label": "Depreciate based on daily pro-rata"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-29 15:39:52.740594",
|
||||
"modified": "2023-11-03 21:30:24.266601",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Finance Book",
|
||||
|
@ -470,6 +470,7 @@
|
||||
"fieldname": "material_request",
|
||||
"fieldtype": "Link",
|
||||
"label": "Material Request",
|
||||
"mandatory_depends_on": "eval: doc.material_request_item",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "prevdoc_docname",
|
||||
"oldfieldtype": "Link",
|
||||
@ -485,6 +486,7 @@
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Material Request Item",
|
||||
"mandatory_depends_on": "eval: doc.material_request",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "prevdoc_detail_docname",
|
||||
"oldfieldtype": "Data",
|
||||
@ -914,7 +916,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-10-27 15:50:42.655573",
|
||||
"modified": "2023-11-06 11:00:53.596417",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
|
@ -174,7 +174,7 @@
|
||||
"fieldname": "supplier_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Supplier Type",
|
||||
"options": "Company\nIndividual",
|
||||
"options": "Company\nIndividual\nProprietorship\nPartnership",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@ -485,7 +485,7 @@
|
||||
"link_fieldname": "party"
|
||||
}
|
||||
],
|
||||
"modified": "2023-09-25 12:48:21.869563",
|
||||
"modified": "2023-10-19 16:55:15.148325",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier",
|
||||
|
@ -2267,6 +2267,7 @@ class AccountsController(TransactionBase):
|
||||
repost_ledger = frappe.new_doc("Repost Accounting Ledger")
|
||||
repost_ledger.company = self.company
|
||||
repost_ledger.append("vouchers", {"voucher_type": self.doctype, "voucher_no": self.name})
|
||||
repost_ledger.flags.ignore_permissions = True
|
||||
repost_ledger.insert()
|
||||
repost_ledger.submit()
|
||||
self.db_set("repost_required", 0)
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import ValidationError, _, msgprint
|
||||
from frappe.contacts.doctype.address.address import get_address_display
|
||||
from frappe.contacts.doctype.address.address import render_address
|
||||
from frappe.utils import cint, flt, getdate
|
||||
from frappe.utils.data import nowtime
|
||||
|
||||
@ -105,26 +105,26 @@ class BuyingController(SubcontractingController):
|
||||
def set_rate_for_standalone_debit_note(self):
|
||||
if self.get("is_return") and self.get("update_stock") and not self.return_against:
|
||||
for row in self.items:
|
||||
if row.rate <= 0:
|
||||
# override the rate with valuation rate
|
||||
row.rate = get_incoming_rate(
|
||||
{
|
||||
"item_code": row.item_code,
|
||||
"warehouse": row.warehouse,
|
||||
"posting_date": self.get("posting_date"),
|
||||
"posting_time": self.get("posting_time"),
|
||||
"qty": row.qty,
|
||||
"serial_and_batch_bundle": row.get("serial_and_batch_bundle"),
|
||||
"company": self.company,
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
},
|
||||
raise_error_if_no_rate=False,
|
||||
)
|
||||
|
||||
# override the rate with valuation rate
|
||||
row.rate = get_incoming_rate(
|
||||
{
|
||||
"item_code": row.item_code,
|
||||
"warehouse": row.warehouse,
|
||||
"posting_date": self.get("posting_date"),
|
||||
"posting_time": self.get("posting_time"),
|
||||
"qty": row.qty,
|
||||
"serial_and_batch_bundle": row.get("serial_and_batch_bundle"),
|
||||
"company": self.company,
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
},
|
||||
raise_error_if_no_rate=False,
|
||||
)
|
||||
|
||||
row.discount_percentage = 0.0
|
||||
row.discount_amount = 0.0
|
||||
row.margin_rate_or_amount = 0.0
|
||||
row.discount_percentage = 0.0
|
||||
row.discount_amount = 0.0
|
||||
row.margin_rate_or_amount = 0.0
|
||||
|
||||
def set_missing_values(self, for_validate=False):
|
||||
super(BuyingController, self).set_missing_values(for_validate)
|
||||
@ -246,7 +246,9 @@ class BuyingController(SubcontractingController):
|
||||
|
||||
for address_field, address_display_field in address_dict.items():
|
||||
if self.get(address_field):
|
||||
self.set(address_display_field, get_address_display(self.get(address_field)))
|
||||
self.set(
|
||||
address_display_field, render_address(self.get(address_field), check_permissions=False)
|
||||
)
|
||||
|
||||
def set_total_in_words(self):
|
||||
from frappe.utils import money_in_words
|
||||
|
@ -47,15 +47,15 @@ status_map = {
|
||||
],
|
||||
[
|
||||
"To Bill",
|
||||
"eval:(self.per_delivered == 100 or self.skip_delivery_note) and self.per_billed < 100 and self.docstatus == 1",
|
||||
"eval:(self.per_delivered >= 100 or self.skip_delivery_note) and self.per_billed < 100 and self.docstatus == 1",
|
||||
],
|
||||
[
|
||||
"To Deliver",
|
||||
"eval:self.per_delivered < 100 and self.per_billed == 100 and self.docstatus == 1 and not self.skip_delivery_note",
|
||||
"eval:self.per_delivered < 100 and self.per_billed >= 100 and self.docstatus == 1 and not self.skip_delivery_note",
|
||||
],
|
||||
[
|
||||
"Completed",
|
||||
"eval:(self.per_delivered == 100 or self.skip_delivery_note) and self.per_billed == 100 and self.docstatus == 1",
|
||||
"eval:(self.per_delivered >= 100 or self.skip_delivery_note) and self.per_billed >= 100 and self.docstatus == 1",
|
||||
],
|
||||
["Cancelled", "eval:self.docstatus==2"],
|
||||
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
|
||||
|
@ -1213,8 +1213,6 @@ def create_item_wise_repost_entries(voucher_type, voucher_no, allow_zero_rate=Fa
|
||||
|
||||
repost_entry = frappe.new_doc("Repost Item Valuation")
|
||||
repost_entry.based_on = "Item and Warehouse"
|
||||
repost_entry.voucher_type = voucher_type
|
||||
repost_entry.voucher_no = voucher_no
|
||||
|
||||
repost_entry.item_code = sle.item_code
|
||||
repost_entry.warehouse = sle.warehouse
|
||||
|
@ -7,7 +7,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.desk.doctype.tag.tag import add_tag
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_months, formatdate, getdate, today
|
||||
from frappe.utils import add_months, formatdate, getdate, sbool, today
|
||||
from plaid.errors import ItemError
|
||||
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
|
||||
@ -237,8 +237,6 @@ def new_bank_transaction(transaction):
|
||||
deposit = abs(amount)
|
||||
withdrawal = 0.0
|
||||
|
||||
status = "Pending" if transaction["pending"] == True else "Settled"
|
||||
|
||||
tags = []
|
||||
if transaction["category"]:
|
||||
try:
|
||||
@ -247,13 +245,14 @@ def new_bank_transaction(transaction):
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if not frappe.db.exists("Bank Transaction", dict(transaction_id=transaction["transaction_id"])):
|
||||
if not frappe.db.exists(
|
||||
"Bank Transaction", dict(transaction_id=transaction["transaction_id"])
|
||||
) and not sbool(transaction["pending"]):
|
||||
try:
|
||||
new_transaction = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Bank Transaction",
|
||||
"date": getdate(transaction["date"]),
|
||||
"status": status,
|
||||
"bank_account": bank_account,
|
||||
"deposit": deposit,
|
||||
"withdrawal": withdrawal,
|
||||
|
@ -36,6 +36,7 @@
|
||||
"prod_plan_references",
|
||||
"section_break_24",
|
||||
"combine_sub_items",
|
||||
"sub_assembly_warehouse",
|
||||
"section_break_ucc4",
|
||||
"skip_available_sub_assembly_item",
|
||||
"column_break_igxl",
|
||||
@ -416,13 +417,19 @@
|
||||
{
|
||||
"fieldname": "column_break_igxl",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "sub_assembly_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Sub Assembly Warehouse",
|
||||
"options": "Warehouse"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-calendar",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-29 11:41:03.246059",
|
||||
"modified": "2023-11-03 14:08:11.928027",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan",
|
||||
|
@ -490,6 +490,12 @@ class ProductionPlan(Document):
|
||||
bin = frappe.get_doc("Bin", bin_name, for_update=True)
|
||||
bin.update_reserved_qty_for_production_plan()
|
||||
|
||||
for d in self.sub_assembly_items:
|
||||
if d.fg_warehouse and d.type_of_manufacturing == "In House":
|
||||
bin_name = get_or_make_bin(d.production_item, d.fg_warehouse)
|
||||
bin = frappe.get_doc("Bin", bin_name, for_update=True)
|
||||
bin.update_reserved_qty_for_for_sub_assembly()
|
||||
|
||||
def delete_draft_work_order(self):
|
||||
for d in frappe.get_all(
|
||||
"Work Order", fields=["name"], filters={"docstatus": 0, "production_plan": ("=", self.name)}
|
||||
@ -809,7 +815,11 @@ class ProductionPlan(Document):
|
||||
|
||||
bom_data = []
|
||||
|
||||
warehouse = row.warehouse if self.skip_available_sub_assembly_item else None
|
||||
warehouse = (
|
||||
(self.sub_assembly_warehouse or row.warehouse)
|
||||
if self.skip_available_sub_assembly_item
|
||||
else None
|
||||
)
|
||||
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty, self.company, warehouse=warehouse)
|
||||
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
|
||||
sub_assembly_items_store.extend(bom_data)
|
||||
@ -831,7 +841,7 @@ class ProductionPlan(Document):
|
||||
for data in bom_data:
|
||||
data.qty = data.stock_qty
|
||||
data.production_plan_item = row.name
|
||||
data.fg_warehouse = row.warehouse
|
||||
data.fg_warehouse = self.sub_assembly_warehouse or row.warehouse
|
||||
data.schedule_date = row.planned_start_date
|
||||
data.type_of_manufacturing = manufacturing_type or (
|
||||
"Subcontract" if data.is_sub_contracted_item else "In House"
|
||||
@ -1637,8 +1647,8 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
|
||||
|
||||
query = query.run()
|
||||
|
||||
if not query:
|
||||
return 0.0
|
||||
if not query or query[0][0] is None:
|
||||
return None
|
||||
|
||||
reserved_qty_for_production_plan = flt(query[0][0])
|
||||
|
||||
@ -1780,3 +1790,29 @@ def sales_order_query(
|
||||
query = query.offset(start)
|
||||
|
||||
return query.run()
|
||||
|
||||
|
||||
def get_reserved_qty_for_sub_assembly(item_code, warehouse):
|
||||
table = frappe.qb.DocType("Production Plan")
|
||||
child = frappe.qb.DocType("Production Plan Sub Assembly Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.inner_join(child)
|
||||
.on(table.name == child.parent)
|
||||
.select(Sum(child.qty - IfNull(child.wo_produced_qty, 0)))
|
||||
.where(
|
||||
(table.docstatus == 1)
|
||||
& (child.production_item == item_code)
|
||||
& (child.fg_warehouse == warehouse)
|
||||
& (table.status.notin(["Completed", "Closed"]))
|
||||
)
|
||||
)
|
||||
|
||||
query = query.run()
|
||||
|
||||
if not query or query[0][0] is None:
|
||||
return None
|
||||
|
||||
qty = flt(query[0][0])
|
||||
return qty if qty > 0 else 0.0
|
||||
|
@ -1042,13 +1042,14 @@ class TestProductionPlan(FrappeTestCase):
|
||||
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
||||
|
||||
self.assertEqual(after_qty - before_qty, 1)
|
||||
|
||||
pln = frappe.get_doc("Production Plan", pln.name)
|
||||
pln.cancel()
|
||||
|
||||
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
|
||||
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
||||
|
||||
pln.reload()
|
||||
self.assertEqual(pln.docstatus, 2)
|
||||
self.assertEqual(after_qty, before_qty)
|
||||
|
||||
def test_resered_qty_for_production_plan_for_work_order(self):
|
||||
@ -1359,6 +1360,93 @@ class TestProductionPlan(FrappeTestCase):
|
||||
if row.item_code == "ChildPart2 For SUB Test":
|
||||
self.assertEqual(row.quantity, 2)
|
||||
|
||||
def test_reserve_sub_assembly_items(self):
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
bom_tree = {
|
||||
"Fininshed Goods Bicycle": {
|
||||
"Frame Assembly": {"Frame": {}},
|
||||
"Chain Assembly": {"Chain": {}},
|
||||
}
|
||||
}
|
||||
parent_bom = create_nested_bom(bom_tree, prefix="")
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
company = "_Test Company"
|
||||
|
||||
sub_assembly_warehouse = create_warehouse("SUB ASSEMBLY WH", company=company)
|
||||
|
||||
for item_code in ["Frame", "Chain"]:
|
||||
make_stock_entry(item_code=item_code, target=warehouse, qty=2, basic_rate=100)
|
||||
|
||||
before_qty = flt(
|
||||
frappe.db.get_value(
|
||||
"Bin",
|
||||
{"item_code": "Frame Assembly", "warehouse": sub_assembly_warehouse},
|
||||
"reserved_qty_for_production_plan",
|
||||
)
|
||||
)
|
||||
|
||||
plan = create_production_plan(
|
||||
item_code=parent_bom.item,
|
||||
planned_qty=2,
|
||||
ignore_existing_ordered_qty=1,
|
||||
do_not_submit=1,
|
||||
skip_available_sub_assembly_item=1,
|
||||
warehouse=warehouse,
|
||||
sub_assembly_warehouse=sub_assembly_warehouse,
|
||||
)
|
||||
|
||||
plan.get_sub_assembly_items()
|
||||
plan.submit()
|
||||
|
||||
after_qty = flt(
|
||||
frappe.db.get_value(
|
||||
"Bin",
|
||||
{"item_code": "Frame Assembly", "warehouse": sub_assembly_warehouse},
|
||||
"reserved_qty_for_production_plan",
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(after_qty, before_qty + 2)
|
||||
|
||||
plan.make_work_order()
|
||||
work_orders = frappe.get_all(
|
||||
"Work Order",
|
||||
fields=["name", "production_item"],
|
||||
filters={"production_plan": plan.name},
|
||||
order_by="creation desc",
|
||||
)
|
||||
|
||||
for d in work_orders:
|
||||
wo_doc = frappe.get_doc("Work Order", d.name)
|
||||
wo_doc.skip_transfer = 1
|
||||
wo_doc.from_wip_warehouse = 1
|
||||
|
||||
wo_doc.wip_warehouse = (
|
||||
warehouse
|
||||
if d.production_item in ["Frame Assembly", "Chain Assembly"]
|
||||
else sub_assembly_warehouse
|
||||
)
|
||||
|
||||
wo_doc.submit()
|
||||
|
||||
if d.production_item == "Frame Assembly":
|
||||
self.assertEqual(wo_doc.fg_warehouse, sub_assembly_warehouse)
|
||||
se_doc = frappe.get_doc(make_se_from_wo(wo_doc.name, "Manufacture", 2))
|
||||
se_doc.submit()
|
||||
|
||||
after_qty = flt(
|
||||
frappe.db.get_value(
|
||||
"Bin",
|
||||
{"item_code": "Frame Assembly", "warehouse": sub_assembly_warehouse},
|
||||
"reserved_qty_for_production_plan",
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(after_qty, before_qty)
|
||||
|
||||
|
||||
def create_production_plan(**args):
|
||||
"""
|
||||
@ -1379,6 +1467,7 @@ def create_production_plan(**args):
|
||||
"ignore_existing_ordered_qty": args.ignore_existing_ordered_qty or 0,
|
||||
"get_items_from": "Sales Order",
|
||||
"skip_available_sub_assembly_item": args.skip_available_sub_assembly_item or 0,
|
||||
"sub_assembly_warehouse": args.sub_assembly_warehouse,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -17,11 +17,10 @@
|
||||
"type_of_manufacturing",
|
||||
"supplier",
|
||||
"work_order_details_section",
|
||||
"work_order",
|
||||
"wo_produced_qty",
|
||||
"purchase_order",
|
||||
"production_plan_item",
|
||||
"column_break_7",
|
||||
"produced_qty",
|
||||
"received_qty",
|
||||
"indent",
|
||||
"section_break_19",
|
||||
@ -52,13 +51,6 @@
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Reference"
|
||||
},
|
||||
{
|
||||
"fieldname": "work_order",
|
||||
"fieldtype": "Link",
|
||||
"label": "Work Order",
|
||||
"options": "Work Order",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_7",
|
||||
"fieldtype": "Column Break"
|
||||
@ -81,7 +73,8 @@
|
||||
{
|
||||
"fieldname": "received_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Received Qty"
|
||||
"label": "Received Qty",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "bom_no",
|
||||
@ -161,12 +154,6 @@
|
||||
"label": "Target Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"fieldname": "produced_qty",
|
||||
"fieldtype": "Data",
|
||||
"label": "Produced Quantity",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "In House",
|
||||
"fieldname": "type_of_manufacturing",
|
||||
@ -209,12 +196,18 @@
|
||||
"label": "Projected Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "wo_produced_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Produced Qty",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-05-22 17:52:34.708879",
|
||||
"modified": "2023-11-03 13:33:42.959387",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan Sub Assembly Item",
|
||||
|
@ -710,7 +710,7 @@ erpnext.work_order = {
|
||||
return new Promise((resolve, reject) => {
|
||||
frappe.prompt({
|
||||
fieldtype: 'Float',
|
||||
label: __('Qty for {0}', [purpose]),
|
||||
label: __('Qty for {0}', [__(purpose)]),
|
||||
fieldname: 'qty',
|
||||
description: __('Max: {0}', [max]),
|
||||
default: max
|
||||
|
@ -293,6 +293,7 @@ class WorkOrder(Document):
|
||||
update_produced_qty_in_so_item(self.sales_order, self.sales_order_item)
|
||||
|
||||
if self.production_plan:
|
||||
self.set_produced_qty_for_sub_assembly_item()
|
||||
self.update_production_plan_status()
|
||||
|
||||
def get_transferred_or_manufactured_qty(self, purpose):
|
||||
@ -569,16 +570,49 @@ class WorkOrder(Document):
|
||||
)
|
||||
|
||||
def update_planned_qty(self):
|
||||
from erpnext.manufacturing.doctype.production_plan.production_plan import (
|
||||
get_reserved_qty_for_sub_assembly,
|
||||
)
|
||||
|
||||
qty_dict = {"planned_qty": get_planned_qty(self.production_item, self.fg_warehouse)}
|
||||
|
||||
if self.production_plan_sub_assembly_item and self.production_plan:
|
||||
qty_dict["reserved_qty_for_production_plan"] = get_reserved_qty_for_sub_assembly(
|
||||
self.production_item, self.fg_warehouse
|
||||
)
|
||||
|
||||
update_bin_qty(
|
||||
self.production_item,
|
||||
self.fg_warehouse,
|
||||
{"planned_qty": get_planned_qty(self.production_item, self.fg_warehouse)},
|
||||
qty_dict,
|
||||
)
|
||||
|
||||
if self.material_request:
|
||||
mr_obj = frappe.get_doc("Material Request", self.material_request)
|
||||
mr_obj.update_requested_qty([self.material_request_item])
|
||||
|
||||
def set_produced_qty_for_sub_assembly_item(self):
|
||||
table = frappe.qb.DocType("Work Order")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(Sum(table.produced_qty))
|
||||
.where(
|
||||
(table.production_plan == self.production_plan)
|
||||
& (table.production_plan_sub_assembly_item == self.production_plan_sub_assembly_item)
|
||||
& (table.docstatus == 1)
|
||||
)
|
||||
).run()
|
||||
|
||||
produced_qty = flt(query[0][0]) if query else 0
|
||||
|
||||
frappe.db.set_value(
|
||||
"Production Plan Sub Assembly Item",
|
||||
self.production_plan_sub_assembly_item,
|
||||
"wo_produced_qty",
|
||||
produced_qty,
|
||||
)
|
||||
|
||||
def update_ordered_qty(self):
|
||||
if (
|
||||
self.production_plan
|
||||
|
@ -345,5 +345,9 @@ erpnext.patches.v14_0.rename_over_order_allowance_field
|
||||
erpnext.patches.v14_0.migrate_delivery_stop_lock_field
|
||||
execute:frappe.db.set_single_value("Payment Reconciliation", "invoice_limit", 50)
|
||||
execute:frappe.db.set_single_value("Payment Reconciliation", "payment_limit", 50)
|
||||
erpnext.patches.v14_0.add_default_for_repost_settings
|
||||
erpnext.patches.v15_0.rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month
|
||||
erpnext.patches.v15_0.rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based
|
||||
erpnext.patches.v15_0.set_reserved_stock_in_bin
|
||||
# below migration patch should always run last
|
||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||
|
@ -3,23 +3,24 @@ import frappe
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("stock", "doctype", "quality_inspection_parameter")
|
||||
params = set()
|
||||
|
||||
# get all distinct parameters from QI readigs table
|
||||
reading_params = frappe.db.get_all(
|
||||
"Quality Inspection Reading", fields=["distinct specification"]
|
||||
)
|
||||
reading_params = [d.specification for d in reading_params]
|
||||
# get all parameters from QI readings table
|
||||
for (p,) in frappe.db.get_all(
|
||||
"Quality Inspection Reading", fields=["specification"], as_list=True
|
||||
):
|
||||
params.add(p.strip())
|
||||
|
||||
# get all distinct parameters from QI Template as some may be unused in QI
|
||||
template_params = frappe.db.get_all(
|
||||
"Item Quality Inspection Parameter", fields=["distinct specification"]
|
||||
)
|
||||
template_params = [d.specification for d in template_params]
|
||||
# get all parameters from QI Template as some may be unused in QI
|
||||
for (p,) in frappe.db.get_all(
|
||||
"Item Quality Inspection Parameter", fields=["specification"], as_list=True
|
||||
):
|
||||
params.add(p.strip())
|
||||
|
||||
params = list(set(reading_params + template_params))
|
||||
# because db primary keys are case insensitive, so duplicates will cause an exception
|
||||
params = set({x.casefold(): x for x in params}.values())
|
||||
|
||||
for parameter in params:
|
||||
if not frappe.db.exists("Quality Inspection Parameter", parameter):
|
||||
frappe.get_doc(
|
||||
{"doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter}
|
||||
).insert(ignore_permissions=True)
|
||||
frappe.get_doc(
|
||||
{"doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter}
|
||||
).insert(ignore_permissions=True)
|
||||
|
12
erpnext/patches/v14_0/add_default_for_repost_settings.py
Normal file
12
erpnext/patches/v14_0/add_default_for_repost_settings.py
Normal file
@ -0,0 +1,12 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
"""
|
||||
Update Repost Accounting Ledger Settings with default values
|
||||
"""
|
||||
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
|
||||
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
|
||||
for x in allowed_types:
|
||||
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
|
||||
repost_settings.save()
|
@ -0,0 +1,21 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
from frappe.model.utils.rename_field import rename_field
|
||||
|
||||
|
||||
def execute():
|
||||
try:
|
||||
rename_field(
|
||||
"Asset Finance Book", "daily_depreciation", "depreciation_amount_based_on_num_days_in_month"
|
||||
)
|
||||
rename_field(
|
||||
"Asset Depreciation Schedule",
|
||||
"daily_depreciation",
|
||||
"depreciation_amount_based_on_num_days_in_month",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
if e.args[0] != 1054:
|
||||
raise
|
@ -0,0 +1,21 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
from frappe.model.utils.rename_field import rename_field
|
||||
|
||||
|
||||
def execute():
|
||||
try:
|
||||
rename_field(
|
||||
"Asset Finance Book", "depreciation_amount_based_on_num_days_in_month", "daily_prorata_based"
|
||||
)
|
||||
rename_field(
|
||||
"Asset Depreciation Schedule",
|
||||
"depreciation_amount_based_on_num_days_in_month",
|
||||
"daily_prorata_based",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
if e.args[0] != 1054:
|
||||
raise
|
24
erpnext/patches/v15_0/set_reserved_stock_in_bin.py
Normal file
24
erpnext/patches/v15_0/set_reserved_stock_in_bin.py
Normal file
@ -0,0 +1,24 @@
|
||||
import frappe
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
|
||||
def execute():
|
||||
sre = frappe.qb.DocType("Stock Reservation Entry")
|
||||
query = (
|
||||
frappe.qb.from_(sre)
|
||||
.select(
|
||||
sre.item_code,
|
||||
sre.warehouse,
|
||||
Sum(sre.reserved_qty - sre.delivered_qty).as_("reserved_stock"),
|
||||
)
|
||||
.where((sre.docstatus == 1) & (sre.status.notin(["Delivered", "Cancelled"])))
|
||||
.groupby(sre.item_code, sre.warehouse)
|
||||
)
|
||||
|
||||
for d in query.run(as_dict=True):
|
||||
frappe.db.set_value(
|
||||
"Bin",
|
||||
{"item_code": d.item_code, "warehouse": d.warehouse},
|
||||
"reserved_stock",
|
||||
d.reserved_stock,
|
||||
)
|
@ -3,10 +3,10 @@
|
||||
|
||||
frappe.ui.form.on('Quality Procedure', {
|
||||
refresh: function(frm) {
|
||||
frm.set_query("procedure","processes", (frm) =>{
|
||||
frm.set_query('procedure', 'processes', (frm) =>{
|
||||
return {
|
||||
filters: {
|
||||
name: ["not in", [frm.parent_quality_procedure, frm.name]]
|
||||
name: ['not in', [frm.parent_quality_procedure, frm.name]]
|
||||
}
|
||||
};
|
||||
});
|
||||
@ -14,7 +14,8 @@ frappe.ui.form.on('Quality Procedure', {
|
||||
frm.set_query('parent_quality_procedure', function(){
|
||||
return {
|
||||
filters: {
|
||||
is_group: 1
|
||||
is_group: 1,
|
||||
name: ['!=', frm.doc.name]
|
||||
}
|
||||
};
|
||||
});
|
||||
|
@ -16,16 +16,13 @@ class QualityProcedure(NestedSet):
|
||||
def on_update(self):
|
||||
NestedSet.on_update(self)
|
||||
self.set_parent()
|
||||
self.remove_parent_from_old_child()
|
||||
self.add_child_to_parent()
|
||||
self.remove_child_from_old_parent()
|
||||
|
||||
def after_insert(self):
|
||||
self.set_parent()
|
||||
|
||||
# add child to parent if missing
|
||||
if self.parent_quality_procedure:
|
||||
parent = frappe.get_doc("Quality Procedure", self.parent_quality_procedure)
|
||||
if not [d for d in parent.processes if d.procedure == self.name]:
|
||||
parent.append("processes", {"procedure": self.name, "process_description": self.name})
|
||||
parent.save()
|
||||
self.add_child_to_parent()
|
||||
|
||||
def on_trash(self):
|
||||
# clear from child table (sub procedures)
|
||||
@ -36,15 +33,6 @@ class QualityProcedure(NestedSet):
|
||||
)
|
||||
NestedSet.on_trash(self, allow_root_deletion=True)
|
||||
|
||||
def set_parent(self):
|
||||
for process in self.processes:
|
||||
# Set parent for only those children who don't have a parent
|
||||
has_parent = frappe.db.get_value(
|
||||
"Quality Procedure", process.procedure, "parent_quality_procedure"
|
||||
)
|
||||
if not has_parent and process.procedure:
|
||||
frappe.db.set_value(self.doctype, process.procedure, "parent_quality_procedure", self.name)
|
||||
|
||||
def check_for_incorrect_child(self):
|
||||
for process in self.processes:
|
||||
if process.procedure:
|
||||
@ -61,6 +49,48 @@ class QualityProcedure(NestedSet):
|
||||
title=_("Invalid Child Procedure"),
|
||||
)
|
||||
|
||||
def set_parent(self):
|
||||
"""Set `Parent Procedure` in `Child Procedures`"""
|
||||
|
||||
for process in self.processes:
|
||||
if process.procedure:
|
||||
if not frappe.db.get_value("Quality Procedure", process.procedure, "parent_quality_procedure"):
|
||||
frappe.db.set_value(
|
||||
"Quality Procedure", process.procedure, "parent_quality_procedure", self.name
|
||||
)
|
||||
|
||||
def remove_parent_from_old_child(self):
|
||||
"""Remove `Parent Procedure` from `Old Child Procedures`"""
|
||||
|
||||
if old_doc := self.get_doc_before_save():
|
||||
if old_child_procedures := set([d.procedure for d in old_doc.processes if d.procedure]):
|
||||
current_child_procedures = set([d.procedure for d in self.processes if d.procedure])
|
||||
|
||||
if removed_child_procedures := list(old_child_procedures.difference(current_child_procedures)):
|
||||
for child_procedure in removed_child_procedures:
|
||||
frappe.db.set_value("Quality Procedure", child_procedure, "parent_quality_procedure", None)
|
||||
|
||||
def add_child_to_parent(self):
|
||||
"""Add `Child Procedure` to `Parent Procedure`"""
|
||||
|
||||
if self.parent_quality_procedure:
|
||||
parent = frappe.get_doc("Quality Procedure", self.parent_quality_procedure)
|
||||
if not [d for d in parent.processes if d.procedure == self.name]:
|
||||
parent.append("processes", {"procedure": self.name, "process_description": self.name})
|
||||
parent.save()
|
||||
|
||||
def remove_child_from_old_parent(self):
|
||||
"""Remove `Child Procedure` from `Old Parent Procedure`"""
|
||||
|
||||
if old_doc := self.get_doc_before_save():
|
||||
if old_parent := old_doc.parent_quality_procedure:
|
||||
if self.parent_quality_procedure != old_parent:
|
||||
parent = frappe.get_doc("Quality Procedure", old_parent)
|
||||
for process in parent.processes:
|
||||
if process.procedure == self.name:
|
||||
parent.remove(process)
|
||||
parent.save()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_children(doctype, parent=None, parent_quality_procedure=None, is_root=False):
|
||||
|
@ -1,56 +1,107 @@
|
||||
# Copyright (c) 2018, Frappe and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from .quality_procedure import add_node
|
||||
|
||||
|
||||
class TestQualityProcedure(unittest.TestCase):
|
||||
class TestQualityProcedure(FrappeTestCase):
|
||||
def test_add_node(self):
|
||||
try:
|
||||
procedure = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Quality Procedure",
|
||||
quality_procedure_name="Test Procedure 1",
|
||||
processes=[dict(process_description="Test Step 1")],
|
||||
)
|
||||
).insert()
|
||||
|
||||
frappe.local.form_dict = frappe._dict(
|
||||
doctype="Quality Procedure",
|
||||
quality_procedure_name="Test Child 1",
|
||||
parent_quality_procedure=procedure.name,
|
||||
cmd="test",
|
||||
is_root="false",
|
||||
)
|
||||
node = add_node()
|
||||
|
||||
procedure.reload()
|
||||
|
||||
self.assertEqual(procedure.is_group, 1)
|
||||
|
||||
# child row created
|
||||
self.assertTrue([d for d in procedure.processes if d.procedure == node.name])
|
||||
|
||||
node.delete()
|
||||
procedure.reload()
|
||||
|
||||
# child unset
|
||||
self.assertFalse([d for d in procedure.processes if d.name == node.name])
|
||||
|
||||
finally:
|
||||
procedure.delete()
|
||||
|
||||
|
||||
def create_procedure():
|
||||
return frappe.get_doc(
|
||||
dict(
|
||||
doctype="Quality Procedure",
|
||||
quality_procedure_name="Test Procedure 1",
|
||||
is_group=1,
|
||||
processes=[dict(process_description="Test Step 1")],
|
||||
procedure = create_procedure(
|
||||
{
|
||||
"quality_procedure_name": "Test Procedure 1",
|
||||
"is_group": 1,
|
||||
"processes": [dict(process_description="Test Step 1")],
|
||||
}
|
||||
)
|
||||
).insert()
|
||||
|
||||
frappe.local.form_dict = frappe._dict(
|
||||
doctype="Quality Procedure",
|
||||
quality_procedure_name="Test Child 1",
|
||||
parent_quality_procedure=procedure.name,
|
||||
cmd="test",
|
||||
is_root="false",
|
||||
)
|
||||
node = add_node()
|
||||
|
||||
procedure.reload()
|
||||
|
||||
self.assertEqual(procedure.is_group, 1)
|
||||
|
||||
# child row created
|
||||
self.assertTrue([d for d in procedure.processes if d.procedure == node.name])
|
||||
|
||||
node.delete()
|
||||
procedure.reload()
|
||||
|
||||
# child unset
|
||||
self.assertFalse([d for d in procedure.processes if d.name == node.name])
|
||||
|
||||
def test_remove_parent_from_old_child(self):
|
||||
child_qp = create_procedure(
|
||||
{
|
||||
"quality_procedure_name": "Test Child 1",
|
||||
"is_group": 0,
|
||||
}
|
||||
)
|
||||
group_qp = create_procedure(
|
||||
{
|
||||
"quality_procedure_name": "Test Group",
|
||||
"is_group": 1,
|
||||
"processes": [dict(procedure=child_qp.name)],
|
||||
}
|
||||
)
|
||||
|
||||
child_qp.reload()
|
||||
self.assertEqual(child_qp.parent_quality_procedure, group_qp.name)
|
||||
|
||||
group_qp.reload()
|
||||
del group_qp.processes[0]
|
||||
group_qp.save()
|
||||
|
||||
child_qp.reload()
|
||||
self.assertEqual(child_qp.parent_quality_procedure, None)
|
||||
|
||||
def remove_child_from_old_parent(self):
|
||||
child_qp = create_procedure(
|
||||
{
|
||||
"quality_procedure_name": "Test Child 1",
|
||||
"is_group": 0,
|
||||
}
|
||||
)
|
||||
group_qp = create_procedure(
|
||||
{
|
||||
"quality_procedure_name": "Test Group",
|
||||
"is_group": 1,
|
||||
"processes": [dict(procedure=child_qp.name)],
|
||||
}
|
||||
)
|
||||
|
||||
group_qp.reload()
|
||||
self.assertTrue([d for d in group_qp.processes if d.procedure == child_qp.name])
|
||||
|
||||
child_qp.reload()
|
||||
self.assertEqual(child_qp.parent_quality_procedure, group_qp.name)
|
||||
|
||||
child_qp.parent_quality_procedure = None
|
||||
child_qp.save()
|
||||
|
||||
group_qp.reload()
|
||||
self.assertFalse([d for d in group_qp.processes if d.procedure == child_qp.name])
|
||||
|
||||
|
||||
def create_procedure(kwargs=None):
|
||||
kwargs = frappe._dict(kwargs or {})
|
||||
|
||||
doc = frappe.new_doc("Quality Procedure")
|
||||
doc.quality_procedure_name = kwargs.quality_procedure_name or "_Test Procedure"
|
||||
doc.is_group = kwargs.is_group or 0
|
||||
|
||||
for process in kwargs.processes or []:
|
||||
doc.append("processes", process)
|
||||
|
||||
doc.insert()
|
||||
|
||||
return doc
|
||||
|
@ -134,7 +134,7 @@
|
||||
"label": "Customer Type",
|
||||
"oldfieldname": "customer_type",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Company\nIndividual",
|
||||
"options": "Company\nIndividual\nProprietorship\nPartnership",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@ -584,7 +584,7 @@
|
||||
"link_fieldname": "party"
|
||||
}
|
||||
],
|
||||
"modified": "2023-09-21 12:23:20.706020",
|
||||
"modified": "2023-10-19 16:56:27.327035",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Customer",
|
||||
|
@ -217,7 +217,15 @@ class SalesOrder(SellingController):
|
||||
|
||||
def validate_with_previous_doc(self):
|
||||
super(SalesOrder, self).validate_with_previous_doc(
|
||||
{"Quotation": {"ref_dn_field": "prevdoc_docname", "compare_fields": [["company", "="]]}}
|
||||
{
|
||||
"Quotation": {"ref_dn_field": "prevdoc_docname", "compare_fields": [["company", "="]]},
|
||||
"Quotation Item": {
|
||||
"ref_dn_field": "quotation_item",
|
||||
"compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]],
|
||||
"is_child_table": True,
|
||||
"allow_duplicate_prev_row_id": True,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")):
|
||||
|
@ -5,20 +5,24 @@
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"warehouse",
|
||||
"item_code",
|
||||
"reserved_qty",
|
||||
"column_break_yreo",
|
||||
"warehouse",
|
||||
"section_break_stag",
|
||||
"actual_qty",
|
||||
"ordered_qty",
|
||||
"indented_qty",
|
||||
"planned_qty",
|
||||
"indented_qty",
|
||||
"ordered_qty",
|
||||
"projected_qty",
|
||||
"column_break_xn5j",
|
||||
"reserved_qty",
|
||||
"reserved_qty_for_production",
|
||||
"reserved_qty_for_sub_contract",
|
||||
"reserved_qty_for_production_plan",
|
||||
"ma_rate",
|
||||
"reserved_stock",
|
||||
"section_break_pmrs",
|
||||
"stock_uom",
|
||||
"fcfs_rate",
|
||||
"column_break_0slj",
|
||||
"valuation_rate",
|
||||
"stock_value"
|
||||
],
|
||||
@ -56,7 +60,7 @@
|
||||
"fieldname": "reserved_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Reserved Quantity",
|
||||
"label": "Reserved Qty",
|
||||
"oldfieldname": "reserved_qty",
|
||||
"oldfieldtype": "Currency",
|
||||
"read_only": 1
|
||||
@ -67,7 +71,7 @@
|
||||
"fieldtype": "Float",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Actual Quantity",
|
||||
"label": "Actual Qty",
|
||||
"oldfieldname": "actual_qty",
|
||||
"oldfieldtype": "Currency",
|
||||
"read_only": 1
|
||||
@ -77,7 +81,7 @@
|
||||
"fieldname": "ordered_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Ordered Quantity",
|
||||
"label": "Ordered Qty",
|
||||
"oldfieldname": "ordered_qty",
|
||||
"oldfieldtype": "Currency",
|
||||
"read_only": 1
|
||||
@ -86,7 +90,7 @@
|
||||
"default": "0.00",
|
||||
"fieldname": "indented_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Requested Quantity",
|
||||
"label": "Requested Qty",
|
||||
"oldfieldname": "indented_qty",
|
||||
"oldfieldtype": "Currency",
|
||||
"read_only": 1
|
||||
@ -116,20 +120,9 @@
|
||||
{
|
||||
"fieldname": "reserved_qty_for_sub_contract",
|
||||
"fieldtype": "Float",
|
||||
"label": "Reserved Qty for sub contract",
|
||||
"label": "Reserved Qty for Subcontract",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "ma_rate",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 1,
|
||||
"label": "Moving Average Rate",
|
||||
"oldfieldname": "ma_rate",
|
||||
"oldfieldtype": "Currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"report_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_uom",
|
||||
"fieldtype": "Link",
|
||||
@ -140,17 +133,6 @@
|
||||
"options": "UOM",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "fcfs_rate",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 1,
|
||||
"label": "FCFS Rate",
|
||||
"oldfieldname": "fcfs_rate",
|
||||
"oldfieldtype": "Currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"report_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "valuation_rate",
|
||||
"fieldtype": "Float",
|
||||
@ -172,13 +154,40 @@
|
||||
"fieldtype": "Float",
|
||||
"label": "Reserved Qty for Production Plan",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_stag",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_yreo",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_xn5j",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_pmrs",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_0slj",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "reserved_stock",
|
||||
"fieldtype": "Float",
|
||||
"label": "Reserved Stock",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"idx": 1,
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2023-05-02 23:26:21.806965",
|
||||
"modified": "2023-11-01 16:51:17.079107",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Bin",
|
||||
|
@ -34,10 +34,15 @@ class Bin(Document):
|
||||
get_reserved_qty_for_production_plan,
|
||||
)
|
||||
|
||||
self.reserved_qty_for_production_plan = get_reserved_qty_for_production_plan(
|
||||
reserved_qty_for_production_plan = get_reserved_qty_for_production_plan(
|
||||
self.item_code, self.warehouse
|
||||
)
|
||||
|
||||
if reserved_qty_for_production_plan is None and not self.reserved_qty_for_production_plan:
|
||||
return
|
||||
|
||||
self.reserved_qty_for_production_plan = flt(reserved_qty_for_production_plan)
|
||||
|
||||
self.db_set(
|
||||
"reserved_qty_for_production_plan",
|
||||
flt(self.reserved_qty_for_production_plan),
|
||||
@ -48,6 +53,29 @@ class Bin(Document):
|
||||
self.set_projected_qty()
|
||||
self.db_set("projected_qty", self.projected_qty, update_modified=True)
|
||||
|
||||
def update_reserved_qty_for_for_sub_assembly(self):
|
||||
from erpnext.manufacturing.doctype.production_plan.production_plan import (
|
||||
get_reserved_qty_for_sub_assembly,
|
||||
)
|
||||
|
||||
reserved_qty_for_production_plan = get_reserved_qty_for_sub_assembly(
|
||||
self.item_code, self.warehouse
|
||||
)
|
||||
|
||||
if reserved_qty_for_production_plan is None and not self.reserved_qty_for_production_plan:
|
||||
return
|
||||
|
||||
self.reserved_qty_for_production_plan = flt(reserved_qty_for_production_plan)
|
||||
self.set_projected_qty()
|
||||
|
||||
self.db_set(
|
||||
{
|
||||
"projected_qty": self.projected_qty,
|
||||
"reserved_qty_for_production_plan": flt(self.reserved_qty_for_production_plan),
|
||||
},
|
||||
update_modified=True,
|
||||
)
|
||||
|
||||
def update_reserved_qty_for_production(self):
|
||||
"""Update qty reserved for production from Production Item tables
|
||||
in open work orders"""
|
||||
@ -148,6 +176,17 @@ class Bin(Document):
|
||||
self.set_projected_qty()
|
||||
self.db_set("projected_qty", self.projected_qty, update_modified=True)
|
||||
|
||||
def update_reserved_stock(self):
|
||||
"""Update `Reserved Stock` on change in Reserved Qty of Stock Reservation Entry"""
|
||||
|
||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||
get_sre_reserved_qty_for_item_and_warehouse,
|
||||
)
|
||||
|
||||
reserved_stock = get_sre_reserved_qty_for_item_and_warehouse(self.item_code, self.warehouse)
|
||||
|
||||
self.db_set("reserved_stock", flt(reserved_stock), update_modified=True)
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_unique("Bin", ["item_code", "warehouse"], constraint_name="unique_item_warehouse")
|
||||
|
@ -365,6 +365,9 @@ class DeliveryNote(SellingController):
|
||||
# Update Stock Reservation Entry `Status` based on `Delivered Qty`.
|
||||
sre_doc.update_status()
|
||||
|
||||
# Update Reserved Stock in Bin.
|
||||
sre_doc.update_reserved_stock_in_bin()
|
||||
|
||||
qty_to_deliver -= qty_can_be_deliver
|
||||
|
||||
if self._action == "cancel":
|
||||
@ -427,6 +430,9 @@ class DeliveryNote(SellingController):
|
||||
# Update Stock Reservation Entry `Status` based on `Delivered Qty`.
|
||||
sre_doc.update_status()
|
||||
|
||||
# Update Reserved Stock in Bin.
|
||||
sre_doc.update_reserved_stock_in_bin()
|
||||
|
||||
qty_to_undelivered -= qty_can_be_undelivered
|
||||
|
||||
def validate_against_stock_reservation_entries(self):
|
||||
|
@ -1029,6 +1029,7 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
|
||||
dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-3)
|
||||
si1 = make_sales_invoice(dn1.name)
|
||||
si1.update_billed_amount_in_delivery_note = True
|
||||
si1.insert()
|
||||
si1.submit()
|
||||
dn1.reload()
|
||||
@ -1037,6 +1038,7 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
|
||||
dn2 = create_delivery_note(is_return=1, return_against=dn.name, qty=-4)
|
||||
si2 = make_sales_invoice(dn2.name)
|
||||
si2.update_billed_amount_in_delivery_note = True
|
||||
si2.insert()
|
||||
si2.submit()
|
||||
dn2.reload()
|
||||
|
@ -562,11 +562,10 @@ class PurchaseReceipt(BuyingController):
|
||||
item=d,
|
||||
)
|
||||
|
||||
elif (
|
||||
d.warehouse not in warehouse_with_no_account
|
||||
or d.rejected_warehouse not in warehouse_with_no_account
|
||||
elif (d.warehouse and d.warehouse not in warehouse_with_no_account) or (
|
||||
d.rejected_warehouse and d.rejected_warehouse not in warehouse_with_no_account
|
||||
):
|
||||
warehouse_with_no_account.append(d.warehouse)
|
||||
warehouse_with_no_account.append(d.warehouse or d.rejected_warehouse)
|
||||
elif (
|
||||
d.item_code not in stock_items
|
||||
and not d.is_fixed_asset
|
||||
|
@ -5,7 +5,7 @@
|
||||
from unittest.mock import MagicMock, call
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, add_to_date, now, nowdate, today
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
@ -200,6 +200,7 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin):
|
||||
|
||||
riv.set_status("Skipped")
|
||||
|
||||
@change_settings("Stock Reposting Settings", {"item_based_reposting": 0})
|
||||
def test_prevention_of_cancelled_transaction_riv(self):
|
||||
frappe.flags.dont_execute_stock_reposts = True
|
||||
|
||||
@ -377,6 +378,7 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin):
|
||||
accounts_settings.acc_frozen_upto = ""
|
||||
accounts_settings.save()
|
||||
|
||||
@change_settings("Stock Reposting Settings", {"item_based_reposting": 0})
|
||||
def test_create_repost_entry_for_cancelled_document(self):
|
||||
pr = make_purchase_receipt(
|
||||
company="_Test Company with perpetual inventory",
|
||||
|
@ -161,7 +161,7 @@ class StockEntry(StockController):
|
||||
if self.is_enqueue_action():
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Draft stage"
|
||||
"The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Entry and revert to the Draft stage"
|
||||
)
|
||||
)
|
||||
self.queue_action("submit", timeout=2000)
|
||||
@ -172,7 +172,7 @@ class StockEntry(StockController):
|
||||
if self.is_enqueue_action():
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Submitted stage"
|
||||
"The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Entry and revert to the Submitted stage"
|
||||
)
|
||||
)
|
||||
self.queue_action("cancel", timeout=2000)
|
||||
|
@ -1471,6 +1471,7 @@ class TestStockEntry(FrappeTestCase):
|
||||
self.assertEqual(se.items[0].item_name, item.item_name)
|
||||
self.assertEqual(se.items[0].stock_uom, item.stock_uom)
|
||||
|
||||
@change_settings("Stock Reposting Settings", {"item_based_reposting": 0})
|
||||
def test_reposting_for_depedent_warehouse(self):
|
||||
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import repost_sl_entries
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
@ -674,6 +674,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
||||
self.assertEqual(flt(sl_entry.actual_qty), 1.0)
|
||||
self.assertEqual(flt(sl_entry.qty_after_transaction), 1.0)
|
||||
|
||||
@change_settings("Stock Reposting Settings", {"item_based_reposting": 0})
|
||||
def test_backdated_stock_reco_entry(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
|
@ -50,7 +50,7 @@
|
||||
"label": "Limit timeslot for Stock Reposting"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"default": "1",
|
||||
"fieldname": "item_based_reposting",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Item based reposting"
|
||||
@ -70,7 +70,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-05-04 16:14:29.080697",
|
||||
"modified": "2023-11-01 16:14:29.080697",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Reposting Settings",
|
||||
|
@ -9,6 +9,8 @@ from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
from erpnext.stock.utils import get_or_make_bin
|
||||
|
||||
|
||||
class StockReservationEntry(Document):
|
||||
def validate(self) -> None:
|
||||
@ -31,6 +33,7 @@ class StockReservationEntry(Document):
|
||||
self.update_reserved_qty_in_voucher()
|
||||
self.update_reserved_qty_in_pick_list()
|
||||
self.update_status()
|
||||
self.update_reserved_stock_in_bin()
|
||||
|
||||
def on_update_after_submit(self) -> None:
|
||||
self.can_be_updated()
|
||||
@ -40,12 +43,14 @@ class StockReservationEntry(Document):
|
||||
self.validate_reservation_based_on_serial_and_batch()
|
||||
self.update_reserved_qty_in_voucher()
|
||||
self.update_status()
|
||||
self.update_reserved_stock_in_bin()
|
||||
self.reload()
|
||||
|
||||
def on_cancel(self) -> None:
|
||||
self.update_reserved_qty_in_voucher()
|
||||
self.update_reserved_qty_in_pick_list()
|
||||
self.update_status()
|
||||
self.update_reserved_stock_in_bin()
|
||||
|
||||
def validate_amended_doc(self) -> None:
|
||||
"""Raises an exception if document is amended."""
|
||||
@ -341,6 +346,13 @@ class StockReservationEntry(Document):
|
||||
update_modified=update_modified,
|
||||
)
|
||||
|
||||
def update_reserved_stock_in_bin(self) -> None:
|
||||
"""Updates `Reserved Stock` in Bin."""
|
||||
|
||||
bin_name = get_or_make_bin(self.item_code, self.warehouse)
|
||||
bin_doc = frappe.get_cached_doc("Bin", bin_name)
|
||||
bin_doc.update_reserved_stock()
|
||||
|
||||
def update_status(self, status: str = None, update_modified: bool = True) -> None:
|
||||
"""Updates status based on Voucher Qty, Reserved Qty and Delivered Qty."""
|
||||
|
||||
@ -681,6 +693,68 @@ def get_sre_reserved_qty_for_voucher_detail_no(
|
||||
return flt(reserved_qty[0][0])
|
||||
|
||||
|
||||
def get_sre_reserved_serial_nos_details(
|
||||
item_code: str, warehouse: str, serial_nos: list = None
|
||||
) -> dict:
|
||||
"""Returns a dict of `Serial No` reserved in Stock Reservation Entry. The dict is like {serial_no: sre_name, ...}"""
|
||||
|
||||
sre = frappe.qb.DocType("Stock Reservation Entry")
|
||||
sb_entry = frappe.qb.DocType("Serial and Batch Entry")
|
||||
query = (
|
||||
frappe.qb.from_(sre)
|
||||
.inner_join(sb_entry)
|
||||
.on(sre.name == sb_entry.parent)
|
||||
.select(sb_entry.serial_no, sre.name)
|
||||
.where(
|
||||
(sre.docstatus == 1)
|
||||
& (sre.item_code == item_code)
|
||||
& (sre.warehouse == warehouse)
|
||||
& (sre.reserved_qty > sre.delivered_qty)
|
||||
& (sre.status.notin(["Delivered", "Cancelled"]))
|
||||
& (sre.reservation_based_on == "Serial and Batch")
|
||||
)
|
||||
.orderby(sb_entry.creation)
|
||||
)
|
||||
|
||||
if serial_nos:
|
||||
query = query.where(sb_entry.serial_no.isin(serial_nos))
|
||||
|
||||
return frappe._dict(query.run())
|
||||
|
||||
|
||||
def get_sre_reserved_batch_nos_details(
|
||||
item_code: str, warehouse: str, batch_nos: list = None
|
||||
) -> dict:
|
||||
"""Returns a dict of `Batch Qty` reserved in Stock Reservation Entry. The dict is like {batch_no: qty, ...}"""
|
||||
|
||||
sre = frappe.qb.DocType("Stock Reservation Entry")
|
||||
sb_entry = frappe.qb.DocType("Serial and Batch Entry")
|
||||
query = (
|
||||
frappe.qb.from_(sre)
|
||||
.inner_join(sb_entry)
|
||||
.on(sre.name == sb_entry.parent)
|
||||
.select(
|
||||
sb_entry.batch_no,
|
||||
Sum(sb_entry.qty - sb_entry.delivered_qty),
|
||||
)
|
||||
.where(
|
||||
(sre.docstatus == 1)
|
||||
& (sre.item_code == item_code)
|
||||
& (sre.warehouse == warehouse)
|
||||
& ((sre.reserved_qty - sre.delivered_qty) > 0)
|
||||
& (sre.status.notin(["Delivered", "Cancelled"]))
|
||||
& (sre.reservation_based_on == "Serial and Batch")
|
||||
)
|
||||
.groupby(sb_entry.batch_no)
|
||||
.orderby(sb_entry.creation)
|
||||
)
|
||||
|
||||
if batch_nos:
|
||||
query = query.where(sb_entry.batch_no.isin(batch_nos))
|
||||
|
||||
return frappe._dict(query.run())
|
||||
|
||||
|
||||
def get_sre_details_for_voucher(voucher_type: str, voucher_no: str) -> list[dict]:
|
||||
"""Returns a list of SREs for the provided voucher."""
|
||||
|
||||
|
@ -286,6 +286,7 @@ class TestStockReservationEntry(FrappeTestCase):
|
||||
self.assertEqual(item.stock_reserved_qty, sre_details.reserved_qty)
|
||||
self.assertEqual(sre_details.status, "Partially Reserved")
|
||||
|
||||
cancel_stock_reservation_entries("Sales Order", so.name)
|
||||
se.cancel()
|
||||
|
||||
# Test - 3: Stock should be fully Reserved if the Available Qty to Reserve is greater than the Un-reserved Qty.
|
||||
@ -493,7 +494,7 @@ class TestStockReservationEntry(FrappeTestCase):
|
||||
"pick_serial_and_batch_based_on": "FIFO",
|
||||
},
|
||||
)
|
||||
def test_stock_reservation_from_pick_list(self):
|
||||
def test_stock_reservation_from_pick_list(self) -> None:
|
||||
items_details = create_items()
|
||||
create_material_receipt(items_details, self.warehouse, qty=100)
|
||||
|
||||
@ -575,7 +576,7 @@ class TestStockReservationEntry(FrappeTestCase):
|
||||
"auto_reserve_stock_for_sales_order_on_purchase": 1,
|
||||
},
|
||||
)
|
||||
def test_stock_reservation_from_purchase_receipt(self):
|
||||
def test_stock_reservation_from_purchase_receipt(self) -> None:
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_material_request
|
||||
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
|
||||
@ -645,6 +646,40 @@ class TestStockReservationEntry(FrappeTestCase):
|
||||
# Test - 3: Reserved Serial/Batch Nos should be equal to PR Item Serial/Batch Nos.
|
||||
self.assertEqual(set(sb_details), set(reserved_sb_details))
|
||||
|
||||
@change_settings(
|
||||
"Stock Settings",
|
||||
{
|
||||
"allow_negative_stock": 0,
|
||||
"enable_stock_reservation": 1,
|
||||
"auto_reserve_serial_and_batch": 1,
|
||||
"pick_serial_and_batch_based_on": "FIFO",
|
||||
},
|
||||
)
|
||||
def test_consider_reserved_stock_while_cancelling_an_inward_transaction(self) -> None:
|
||||
items_details = create_items()
|
||||
se = create_material_receipt(items_details, self.warehouse, qty=100)
|
||||
|
||||
item_list = []
|
||||
for item_code, properties in items_details.items():
|
||||
item_list.append(
|
||||
{
|
||||
"item_code": item_code,
|
||||
"warehouse": self.warehouse,
|
||||
"qty": randint(11, 100),
|
||||
"uom": properties.stock_uom,
|
||||
"rate": randint(10, 400),
|
||||
}
|
||||
)
|
||||
|
||||
so = make_sales_order(
|
||||
item_list=item_list,
|
||||
warehouse=self.warehouse,
|
||||
)
|
||||
so.create_stock_reservation_entries()
|
||||
|
||||
# Test - 1: ValidationError should be thrown as the inwarded stock is reserved.
|
||||
self.assertRaises(frappe.ValidationError, se.cancel)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
cancel_all_stock_reservation_entries()
|
||||
return super().tearDown()
|
||||
|
@ -11,6 +11,7 @@ frappe.pages['stock-balance'].on_page_load = function(wrapper) {
|
||||
label: __('Warehouse'),
|
||||
fieldtype:'Link',
|
||||
options:'Warehouse',
|
||||
default: frappe.route_options && frappe.route_options.warehouse,
|
||||
change: function() {
|
||||
page.item_dashboard.start = 0;
|
||||
page.item_dashboard.refresh();
|
||||
@ -22,6 +23,7 @@ frappe.pages['stock-balance'].on_page_load = function(wrapper) {
|
||||
label: __('Item'),
|
||||
fieldtype:'Link',
|
||||
options:'Item',
|
||||
default: frappe.route_options && frappe.route_options.item_code,
|
||||
change: function() {
|
||||
page.item_dashboard.start = 0;
|
||||
page.item_dashboard.refresh();
|
||||
@ -33,6 +35,7 @@ frappe.pages['stock-balance'].on_page_load = function(wrapper) {
|
||||
label: __('Item Group'),
|
||||
fieldtype:'Link',
|
||||
options:'Item Group',
|
||||
default: frappe.route_options && frappe.route_options.item_group,
|
||||
change: function() {
|
||||
page.item_dashboard.start = 0;
|
||||
page.item_dashboard.refresh();
|
||||
|
@ -11,17 +11,22 @@ from frappe import _, scrub
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.functions import CombineDatetime, Sum
|
||||
from frappe.utils import cint, flt, get_link_to_form, getdate, now, nowdate, parse_json
|
||||
from frappe.utils import cint, flt, get_link_to_form, getdate, now, nowdate, nowtime, parse_json
|
||||
|
||||
import erpnext
|
||||
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
|
||||
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
get_available_batches,
|
||||
)
|
||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||
get_sre_reserved_qty_for_item_and_warehouse as get_reserved_stock,
|
||||
get_sre_reserved_batch_nos_details,
|
||||
get_sre_reserved_serial_nos_details,
|
||||
)
|
||||
from erpnext.stock.utils import (
|
||||
get_incoming_outgoing_rate_for_cancel,
|
||||
get_or_make_bin,
|
||||
get_stock_balance,
|
||||
get_valuation_method,
|
||||
)
|
||||
from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
|
||||
@ -88,6 +93,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
|
||||
is_stock_item = frappe.get_cached_value("Item", args.get("item_code"), "is_stock_item")
|
||||
if is_stock_item:
|
||||
bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
|
||||
args.reserved_stock = flt(frappe.db.get_value("Bin", bin_name, "reserved_stock"))
|
||||
repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
|
||||
update_bin_qty(bin_name, args)
|
||||
else:
|
||||
@ -114,6 +120,7 @@ def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_vou
|
||||
"voucher_no": args.get("voucher_no"),
|
||||
"sle_id": args.get("name"),
|
||||
"creation": args.get("creation"),
|
||||
"reserved_stock": args.get("reserved_stock"),
|
||||
},
|
||||
allow_negative_stock=allow_negative_stock,
|
||||
via_landed_cost_voucher=via_landed_cost_voucher,
|
||||
@ -506,7 +513,7 @@ class update_entries_after(object):
|
||||
self.new_items_found = False
|
||||
self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
|
||||
self.affected_transactions: Set[Tuple[str, str]] = set()
|
||||
self.reserved_stock = get_reserved_stock(self.args.item_code, self.args.warehouse)
|
||||
self.reserved_stock = flt(self.args.reserved_stock)
|
||||
|
||||
self.data = frappe._dict()
|
||||
self.initialize_previous_data(self.args)
|
||||
@ -1709,22 +1716,23 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
|
||||
|
||||
frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
|
||||
|
||||
if not args.batch_no:
|
||||
return
|
||||
if args.batch_no:
|
||||
neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
|
||||
if is_negative_with_precision(neg_batch_sle, is_batch=True):
|
||||
message = _(
|
||||
"{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
|
||||
).format(
|
||||
abs(neg_batch_sle[0]["cumulative_total"]),
|
||||
frappe.get_desk_link("Batch", args.batch_no),
|
||||
frappe.get_desk_link("Warehouse", args.warehouse),
|
||||
neg_batch_sle[0]["posting_date"],
|
||||
neg_batch_sle[0]["posting_time"],
|
||||
frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
|
||||
)
|
||||
frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
|
||||
|
||||
neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
|
||||
if is_negative_with_precision(neg_batch_sle, is_batch=True):
|
||||
message = _(
|
||||
"{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
|
||||
).format(
|
||||
abs(neg_batch_sle[0]["cumulative_total"]),
|
||||
frappe.get_desk_link("Batch", args.batch_no),
|
||||
frappe.get_desk_link("Warehouse", args.warehouse),
|
||||
neg_batch_sle[0]["posting_date"],
|
||||
neg_batch_sle[0]["posting_time"],
|
||||
frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
|
||||
)
|
||||
frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
|
||||
if args.reserved_stock:
|
||||
validate_reserved_stock(args)
|
||||
|
||||
|
||||
def is_negative_with_precision(neg_sle, is_batch=False):
|
||||
@ -1791,6 +1799,96 @@ def get_future_sle_with_negative_batch_qty(args):
|
||||
)
|
||||
|
||||
|
||||
def validate_reserved_stock(kwargs):
|
||||
if kwargs.serial_no:
|
||||
serial_nos = kwargs.serial_no.split("\n")
|
||||
validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
|
||||
|
||||
elif kwargs.batch_no:
|
||||
validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, [kwargs.batch_no])
|
||||
|
||||
elif kwargs.serial_and_batch_bundle:
|
||||
sbb_entries = frappe.db.get_all(
|
||||
"Serial and Batch Entry",
|
||||
{
|
||||
"parenttype": "Serial and Batch Bundle",
|
||||
"parent": kwargs.serial_and_batch_bundle,
|
||||
"docstatus": 1,
|
||||
},
|
||||
["batch_no", "serial_no"],
|
||||
)
|
||||
|
||||
if serial_nos := [entry.serial_no for entry in sbb_entries if entry.serial_no]:
|
||||
validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
|
||||
elif batch_nos := [entry.batch_no for entry in sbb_entries if entry.batch_no]:
|
||||
validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, batch_nos)
|
||||
|
||||
# Qty based validation for non-serial-batch items OR SRE with Reservation Based On Qty.
|
||||
precision = cint(frappe.db.get_default("float_precision")) or 2
|
||||
balance_qty = get_stock_balance(kwargs.item_code, kwargs.warehouse)
|
||||
|
||||
diff = flt(balance_qty - kwargs.get("reserved_stock", 0), precision)
|
||||
if diff < 0 and abs(diff) > 0.0001:
|
||||
msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
|
||||
abs(diff),
|
||||
frappe.get_desk_link("Item", kwargs.item_code),
|
||||
frappe.get_desk_link("Warehouse", kwargs.warehouse),
|
||||
nowdate(),
|
||||
nowtime(),
|
||||
)
|
||||
frappe.throw(msg, title=_("Reserved Stock"))
|
||||
|
||||
|
||||
def validate_reserved_serial_nos(item_code, warehouse, serial_nos):
|
||||
if reserved_serial_nos_details := get_sre_reserved_serial_nos_details(
|
||||
item_code, warehouse, serial_nos
|
||||
):
|
||||
if common_serial_nos := list(
|
||||
set(serial_nos).intersection(set(reserved_serial_nos_details.keys()))
|
||||
):
|
||||
msg = _(
|
||||
"Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding."
|
||||
)
|
||||
msg += "<br />"
|
||||
msg += _("Example: Serial No {0} reserved in {1}.").format(
|
||||
frappe.bold(common_serial_nos[0]),
|
||||
frappe.get_desk_link(
|
||||
"Stock Reservation Entry", reserved_serial_nos_details[common_serial_nos[0]]
|
||||
),
|
||||
)
|
||||
frappe.throw(msg, title=_("Reserved Serial No."))
|
||||
|
||||
|
||||
def validate_reserved_batch_nos(item_code, warehouse, batch_nos):
|
||||
if reserved_batches_map := get_sre_reserved_batch_nos_details(item_code, warehouse, batch_nos):
|
||||
available_batches = get_available_batches(
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": item_code,
|
||||
"warehouse": warehouse,
|
||||
"posting_date": nowdate(),
|
||||
"posting_time": nowtime(),
|
||||
}
|
||||
)
|
||||
)
|
||||
available_batches_map = {row.batch_no: row.qty for row in available_batches}
|
||||
precision = cint(frappe.db.get_default("float_precision")) or 2
|
||||
|
||||
for batch_no in batch_nos:
|
||||
diff = flt(
|
||||
available_batches_map.get(batch_no, 0) - reserved_batches_map.get(batch_no, 0), precision
|
||||
)
|
||||
if diff < 0 and abs(diff) > 0.0001:
|
||||
msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
|
||||
abs(diff),
|
||||
frappe.get_desk_link("Batch", batch_no),
|
||||
frappe.get_desk_link("Warehouse", warehouse),
|
||||
nowdate(),
|
||||
nowtime(),
|
||||
)
|
||||
frappe.throw(msg, title=_("Reserved Stock for Batch"))
|
||||
|
||||
|
||||
def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
|
||||
if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
|
||||
return True
|
||||
|
@ -192,9 +192,7 @@ def mark_retrired_transaction(log_doc, doc_name):
|
||||
record = 0
|
||||
for d in log_doc.get("logger_data"):
|
||||
if d.transaction_name == doc_name and d.transaction_status == "Failed":
|
||||
d.retried = 1
|
||||
frappe.db.set_value("Bulk Transaction Log Detail", d.name, "retried", 1)
|
||||
record = record + 1
|
||||
|
||||
log_doc.save()
|
||||
|
||||
return record
|
||||
|
Loading…
x
Reference in New Issue
Block a user