mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-25 13:45:04 +00:00
Compare commits
146 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f261f3448 | ||
|
|
11a6ebaeef | ||
|
|
d2b2002664 | ||
|
|
56dad7d365 | ||
|
|
6869e5dde9 | ||
|
|
5d5ec2ab7c | ||
|
|
f5a4ec129b | ||
|
|
e185a06a15 | ||
|
|
193d7981ea | ||
|
|
957eabf53e | ||
|
|
3bb186736d | ||
|
|
1121c6663f | ||
|
|
944479313c | ||
|
|
daa75eea00 | ||
|
|
a2b7fc18ab | ||
|
|
7d6984c873 | ||
|
|
64cbf446bd | ||
|
|
b99cdb5be7 | ||
|
|
d6de50634f | ||
|
|
4d7c0c004a | ||
|
|
f0e3fb466a | ||
|
|
8337439589 | ||
|
|
88e5ed7998 | ||
|
|
fee2255661 | ||
|
|
23d91145d0 | ||
|
|
6c8e0fd1fb | ||
|
|
512a171ad5 | ||
|
|
30f034555b | ||
|
|
a35ce12d60 | ||
|
|
8cf057849e | ||
|
|
f3c60ea0a7 | ||
|
|
c724573a18 | ||
|
|
53e1b57354 | ||
|
|
42e7725442 | ||
|
|
18b7af977c | ||
|
|
db746a4def | ||
|
|
3d5fb5fc90 | ||
|
|
a833010d2b | ||
|
|
0d6148f218 | ||
|
|
6d51d14dfd | ||
|
|
cb2cb4447a | ||
|
|
b3a8fe9391 | ||
|
|
2a820a85ed | ||
|
|
8d8dd0cd2b | ||
|
|
3a5f19853a | ||
|
|
c0dd794e15 | ||
|
|
f56ee58e81 | ||
|
|
2c99075899 | ||
|
|
01cd509113 | ||
|
|
5563bea789 | ||
|
|
a0a932a235 | ||
|
|
6f75a3c617 | ||
|
|
a4d3934f75 | ||
|
|
b4171e4bd9 | ||
|
|
76d32ab07a | ||
|
|
3d469db47b | ||
|
|
80244bafa4 | ||
|
|
c54e97b89a | ||
|
|
14202fae06 | ||
|
|
9fc0ac1a92 | ||
|
|
5bdd2989c6 | ||
|
|
42dccadff1 | ||
|
|
0650c22b53 | ||
|
|
ec26c92263 | ||
|
|
26248924b6 | ||
|
|
8d29dc6a81 | ||
|
|
6eb50fa300 | ||
|
|
8c350d43b2 | ||
|
|
caa6ca1d0b | ||
|
|
47f1714be4 | ||
|
|
ecb0506dba | ||
|
|
42382b3945 | ||
|
|
993114942e | ||
|
|
5a28a1728e | ||
|
|
80a5df0e96 | ||
|
|
25193c5e92 | ||
|
|
1ee14ac135 | ||
|
|
a3e5ffe915 | ||
|
|
ff868a9290 | ||
|
|
247ae7a965 | ||
|
|
3a149b3c9b | ||
|
|
08bed618f6 | ||
|
|
d9ca680a29 | ||
|
|
7401dc4015 | ||
|
|
eb7e063d5c | ||
|
|
0dbe79645c | ||
|
|
995773088a | ||
|
|
be736cf641 | ||
|
|
85089d3d64 | ||
|
|
eed6d2b81c | ||
|
|
edf53f1ab7 | ||
|
|
758282739e | ||
|
|
49d995c3ac | ||
|
|
72ca2ec9a5 | ||
|
|
4297895bd9 | ||
|
|
7428df8778 | ||
|
|
767c79663c | ||
|
|
fa85482662 | ||
|
|
831e2aaf18 | ||
|
|
9ac665b4bd | ||
|
|
8d8d84bae4 | ||
|
|
9f4cb98de6 | ||
|
|
72c16097d6 | ||
|
|
2203ea9301 | ||
|
|
28c9f2adab | ||
|
|
d04f7ffe87 | ||
|
|
21e5c01f11 | ||
|
|
ae6c1a30ac | ||
|
|
1040198ce1 | ||
|
|
69878b7847 | ||
|
|
ec0201cb85 | ||
|
|
5d7fb1d945 | ||
|
|
33542cb909 | ||
|
|
549dc286d0 | ||
|
|
f547befeb9 | ||
|
|
a144059c7c | ||
|
|
b96361e837 | ||
|
|
cd5994017c | ||
|
|
431fb62803 | ||
|
|
9e9de4c99e | ||
|
|
d3369368db | ||
|
|
b24de3e35b | ||
|
|
0344442d42 | ||
|
|
0decd0955b | ||
|
|
6f7fdbefac | ||
|
|
dbd466b6b2 | ||
|
|
4914481105 | ||
|
|
8514c01a91 | ||
|
|
751c20984f | ||
|
|
528f42c713 | ||
|
|
7cc7179b05 | ||
|
|
cfaad685a4 | ||
|
|
e420fa9779 | ||
|
|
0f9849e672 | ||
|
|
3be037221a | ||
|
|
b2e108afcc | ||
|
|
114a5f8cca | ||
|
|
aba54ba18f | ||
|
|
6a7768367e | ||
|
|
3a1ad6e844 | ||
|
|
40abd82e2d | ||
|
|
2933a86458 | ||
|
|
ecf0d0b388 | ||
|
|
b84ca04975 | ||
|
|
abb88662c1 | ||
|
|
976abf7b3c |
@@ -2,8 +2,9 @@ import functools
|
||||
import inspect
|
||||
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.33.2"
|
||||
__version__ = "15.34.1"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
@@ -149,3 +150,13 @@ def allow_regional(fn):
|
||||
return frappe.get_attr(overrides[function_path][-1])(*args, **kwargs)
|
||||
|
||||
return caller
|
||||
|
||||
|
||||
def check_app_permission():
|
||||
if frappe.session.user == "Administrator":
|
||||
return True
|
||||
|
||||
if is_website_user():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -202,7 +202,7 @@ class Account(NestedSet):
|
||||
msg = _(
|
||||
"There are ledger entries against this account. Changing {0} to non-{1} in live system will cause incorrect output in 'Accounts {2}' report"
|
||||
).format(
|
||||
frappe.bold("Account Type"), doc_before_save.account_type, doc_before_save.account_type
|
||||
frappe.bold(_("Account Type")), doc_before_save.account_type, doc_before_save.account_type
|
||||
)
|
||||
frappe.msgprint(msg)
|
||||
self.add_comment("Comment", msg)
|
||||
|
||||
@@ -38,6 +38,11 @@ frappe.ui.form.on("Bank Clearance", {
|
||||
frm.add_custom_button(__("Get Payment Entries"), () => frm.trigger("get_payment_entries"));
|
||||
|
||||
frm.change_custom_button_type(__("Get Payment Entries"), null, "primary");
|
||||
if (frm.doc.payment_entries.length) {
|
||||
frm.add_custom_button(__("Update Clearance Date"), () => frm.trigger("update_clearance_date"));
|
||||
frm.change_custom_button_type(__("Get Payment Entries"), null, "default");
|
||||
frm.change_custom_button_type(__("Update Clearance Date"), null, "primary");
|
||||
}
|
||||
},
|
||||
|
||||
update_clearance_date: function (frm) {
|
||||
@@ -45,13 +50,7 @@ frappe.ui.form.on("Bank Clearance", {
|
||||
method: "update_clearance_date",
|
||||
doc: frm.doc,
|
||||
callback: function (r, rt) {
|
||||
frm.refresh_field("payment_entries");
|
||||
frm.refresh_fields();
|
||||
|
||||
if (!frm.doc.payment_entries.length) {
|
||||
frm.change_custom_button_type(__("Get Payment Entries"), null, "primary");
|
||||
frm.change_custom_button_type(__("Update Clearance Date"), null, "default");
|
||||
}
|
||||
frm.refresh();
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -60,17 +59,8 @@ frappe.ui.form.on("Bank Clearance", {
|
||||
return frappe.call({
|
||||
method: "get_payment_entries",
|
||||
doc: frm.doc,
|
||||
callback: function (r, rt) {
|
||||
frm.refresh_field("payment_entries");
|
||||
|
||||
if (frm.doc.payment_entries.length) {
|
||||
frm.add_custom_button(__("Update Clearance Date"), () =>
|
||||
frm.trigger("update_clearance_date")
|
||||
);
|
||||
|
||||
frm.change_custom_button_type(__("Get Payment Entries"), null, "default");
|
||||
frm.change_custom_button_type(__("Update Clearance Date"), null, "primary");
|
||||
}
|
||||
callback: function () {
|
||||
frm.refresh();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt, fmt_money, getdate
|
||||
from frappe.utils import flt, fmt_money, get_link_to_form, getdate
|
||||
from pypika import Order
|
||||
|
||||
import erpnext
|
||||
@@ -96,8 +96,11 @@ class BankClearance(Document):
|
||||
|
||||
if d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Clearance date {1} cannot be before Cheque Date {2}").format(
|
||||
d.idx, d.clearance_date, d.cheque_date
|
||||
_("Row #{0}: For {1} Clearance date {2} cannot be before Cheque Date {3}").format(
|
||||
d.idx,
|
||||
get_link_to_form(d.payment_document, d.payment_entry),
|
||||
d.clearance_date,
|
||||
d.cheque_date,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -35,6 +35,11 @@ frappe.ui.form.on("Payment Entry", {
|
||||
var account_types = ["Pay", "Internal Transfer"].includes(frm.doc.payment_type)
|
||||
? ["Bank", "Cash"]
|
||||
: [frappe.boot.party_account_types[frm.doc.party_type]];
|
||||
|
||||
if (frm.doc.party_type == "Shareholder") {
|
||||
account_types.push("Equity");
|
||||
}
|
||||
|
||||
return {
|
||||
filters: {
|
||||
account_type: ["in", account_types],
|
||||
@@ -90,6 +95,9 @@ frappe.ui.form.on("Payment Entry", {
|
||||
var account_types = ["Receive", "Internal Transfer"].includes(frm.doc.payment_type)
|
||||
? ["Bank", "Cash"]
|
||||
: [frappe.boot.party_account_types[frm.doc.party_type]];
|
||||
if (frm.doc.party_type == "Shareholder") {
|
||||
account_types.push("Equity");
|
||||
}
|
||||
return {
|
||||
filters: {
|
||||
account_type: ["in", account_types],
|
||||
@@ -297,7 +305,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
set_dynamic_labels: function (frm) {
|
||||
var company_currency = frm.doc.company
|
||||
? frappe.get_doc(":Company", frm.doc.company).default_currency
|
||||
? frappe.get_doc(":Company", frm.doc.company)?.default_currency
|
||||
: "";
|
||||
|
||||
frm.set_currency_labels(
|
||||
@@ -412,6 +420,12 @@ frappe.ui.form.on("Payment Entry", {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.employee_query",
|
||||
};
|
||||
} else if (frm.doc.party_type == "Shareholder") {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -644,7 +658,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.set_value("source_exchange_rate", 1);
|
||||
} else if (frm.doc.paid_from) {
|
||||
if (["Internal Transfer", "Pay"].includes(frm.doc.payment_type)) {
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company)?.default_currency;
|
||||
frappe.call({
|
||||
method: "erpnext.setup.utils.get_exchange_rate",
|
||||
args: {
|
||||
|
||||
@@ -1740,7 +1740,7 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
|
||||
|
||||
# Get negative outstanding sales /purchase invoices
|
||||
if args.get("party_type") != "Employee" and not args.get("voucher_no"):
|
||||
if args.get("party_type") != "Employee":
|
||||
negative_outstanding_invoices = get_negative_outstanding_invoices(
|
||||
args.get("party_type"),
|
||||
args.get("party"),
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"transaction_date",
|
||||
"column_break_2",
|
||||
"naming_series",
|
||||
"company",
|
||||
"mode_of_payment",
|
||||
"party_details",
|
||||
"party_type",
|
||||
@@ -390,13 +391,20 @@
|
||||
"options": "Payment Request",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-20 13:54:55.245774",
|
||||
"modified": "2024-08-07 16:39:54.288002",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Request",
|
||||
|
||||
@@ -15,7 +15,7 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
)
|
||||
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
||||
from erpnext.accounts.party import get_party_account, get_party_bank_account
|
||||
from erpnext.accounts.utils import get_account_currency
|
||||
from erpnext.accounts.utils import get_account_currency, get_currency_precision
|
||||
from erpnext.utilities import payment_app_import_guard
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ class PaymentRequest(Document):
|
||||
subscription_plans: DF.Table[SubscriptionPlanDetail]
|
||||
swift_number: DF.ReadOnly | None
|
||||
transaction_date: DF.Date | None
|
||||
company: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
@@ -491,6 +492,7 @@ def make_payment_request(**args):
|
||||
"message": gateway_account.get("message") or get_dummy_message(ref_doc),
|
||||
"reference_doctype": args.dt,
|
||||
"reference_name": args.dn,
|
||||
"company": ref_doc.get("company"),
|
||||
"party_type": args.get("party_type") or "Customer",
|
||||
"party": args.get("party") or ref_doc.get("customer"),
|
||||
"bank_account": bank_account,
|
||||
@@ -552,7 +554,7 @@ def get_amount(ref_doc, payment_account=None):
|
||||
grand_total = ref_doc.outstanding_amount
|
||||
|
||||
if grand_total > 0:
|
||||
return grand_total
|
||||
return flt(grand_total, get_currency_precision())
|
||||
else:
|
||||
frappe.throw(_("Payment Entry is already created"))
|
||||
|
||||
|
||||
@@ -87,19 +87,15 @@ class POSClosingEntry(StatusUpdater):
|
||||
as_dict=1,
|
||||
)[0]
|
||||
if pos_invoice.consolidated_invoice:
|
||||
invalid_row.setdefault("msg", []).append(
|
||||
_("POS Invoice is {}").format(frappe.bold("already consolidated"))
|
||||
)
|
||||
invalid_row.setdefault("msg", []).append(_("POS Invoice is already consolidated"))
|
||||
invalid_rows.append(invalid_row)
|
||||
continue
|
||||
if pos_invoice.pos_profile != self.pos_profile:
|
||||
invalid_row.setdefault("msg", []).append(
|
||||
_("POS Profile doesn't matches {}").format(frappe.bold(self.pos_profile))
|
||||
_("POS Profile doesn't match {}").format(frappe.bold(self.pos_profile))
|
||||
)
|
||||
if pos_invoice.docstatus != 1:
|
||||
invalid_row.setdefault("msg", []).append(
|
||||
_("POS Invoice is not {}").format(frappe.bold("submitted"))
|
||||
)
|
||||
invalid_row.setdefault("msg", []).append(_("POS Invoice is not submitted"))
|
||||
if pos_invoice.owner != self.user:
|
||||
invalid_row.setdefault("msg", []).append(
|
||||
_("POS Invoice isn't created by user {}").format(frappe.bold(self.owner))
|
||||
|
||||
@@ -188,7 +188,7 @@ class POSInvoice(SalesInvoice):
|
||||
def validate(self):
|
||||
if not cint(self.is_pos):
|
||||
frappe.throw(
|
||||
_("POS Invoice should have {} field checked.").format(frappe.bold("Include Payment"))
|
||||
_("POS Invoice should have the field {0} checked.").format(frappe.bold(_("Include Payment")))
|
||||
)
|
||||
|
||||
# run on validate method of selling controller
|
||||
|
||||
@@ -97,16 +97,15 @@ class POSInvoiceMergeLog(Document):
|
||||
return_against_status = frappe.db.get_value("POS Invoice", return_against, "status")
|
||||
if return_against_status != "Consolidated":
|
||||
# if return entry is not getting merged in the current pos closing and if it is not consolidated
|
||||
bold_unconsolidated = frappe.bold("not Consolidated")
|
||||
msg = _("Row #{}: Original Invoice {} of return invoice {} is {}.").format(
|
||||
d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated
|
||||
)
|
||||
msg = _(
|
||||
"Row #{}: The original Invoice {} of return invoice {} is not consolidated."
|
||||
).format(d.idx, bold_return_against, bold_pos_invoice)
|
||||
msg += " "
|
||||
msg += _(
|
||||
"Original invoice should be consolidated before or along with the return invoice."
|
||||
"The original invoice should be consolidated before or along with the return invoice."
|
||||
)
|
||||
msg += "<br><br>"
|
||||
msg += _("You can add original invoice {} manually to proceed.").format(
|
||||
msg += _("You can add the original invoice {} manually to proceed.").format(
|
||||
bold_return_against
|
||||
)
|
||||
frappe.throw(msg)
|
||||
|
||||
@@ -186,7 +186,8 @@ class PricingRule(Document):
|
||||
if not self.priority:
|
||||
throw(
|
||||
_("As the field {0} is enabled, the field {1} is mandatory.").format(
|
||||
frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority")
|
||||
frappe.bold(_("Apply Discount on Discounted Rate")),
|
||||
frappe.bold(_("Priority")),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -194,7 +195,7 @@ class PricingRule(Document):
|
||||
throw(
|
||||
_(
|
||||
"As the field {0} is enabled, the value of the field {1} should be more than 1."
|
||||
).format(frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority"))
|
||||
).format(frappe.bold(_("Apply Discount on Discounted Rate")), frappe.bold(_("Priority")))
|
||||
)
|
||||
|
||||
def validate_applicable_for_selling_or_buying(self):
|
||||
|
||||
@@ -486,7 +486,7 @@ def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules, row_item):
|
||||
continue
|
||||
|
||||
stock_qty = row.get("qty") * (row.get("conversion_factor") or 1.0)
|
||||
amount = stock_qty * (row.get("price_list_rate") or row.get("rate"))
|
||||
amount = stock_qty * (flt(row.get("price_list_rate")) or flt(row.get("rate")))
|
||||
pricing_rules = filter_pricing_rules_for_qty_amount(stock_qty, amount, pricing_rules, row)
|
||||
|
||||
if pricing_rules and pricing_rules[0]:
|
||||
|
||||
@@ -377,16 +377,16 @@ class PurchaseInvoice(BuyingController):
|
||||
if account.report_type != "Balance Sheet":
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please ensure {} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account."
|
||||
).format(frappe.bold("Credit To")),
|
||||
"Please ensure that the {0} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account."
|
||||
).format(frappe.bold(_("Credit To"))),
|
||||
title=_("Invalid Account"),
|
||||
)
|
||||
|
||||
if self.supplier and account.account_type != "Payable":
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please ensure {} account {} is a Payable account. Change the account type to Payable or select a different account."
|
||||
).format(frappe.bold("Credit To"), frappe.bold(self.credit_to)),
|
||||
"Please ensure that the {0} account {1} is a Payable account. You can change the account type to Payable or select a different account."
|
||||
).format(frappe.bold(_("Credit To")), frappe.bold(self.credit_to)),
|
||||
title=_("Invalid Account"),
|
||||
)
|
||||
|
||||
@@ -634,7 +634,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"To submit the invoice without purchase order please set {0} as {1} in {2}"
|
||||
).format(
|
||||
frappe.bold(_("Purchase Order Required")),
|
||||
frappe.bold("No"),
|
||||
frappe.bold(_("No")),
|
||||
get_link_to_form("Buying Settings", "Buying Settings", "Buying Settings"),
|
||||
)
|
||||
throw(msg, title=_("Mandatory Purchase Order"))
|
||||
@@ -655,7 +655,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"To submit the invoice without purchase receipt please set {0} as {1} in {2}"
|
||||
).format(
|
||||
frappe.bold(_("Purchase Receipt Required")),
|
||||
frappe.bold("No"),
|
||||
frappe.bold(_("No")),
|
||||
get_link_to_form("Buying Settings", "Buying Settings", "Buying Settings"),
|
||||
)
|
||||
throw(msg, title=_("Mandatory Purchase Receipt"))
|
||||
@@ -1262,7 +1262,11 @@ class PurchaseInvoice(BuyingController):
|
||||
def update_gross_purchase_amount_for_linked_assets(self, item):
|
||||
assets = frappe.db.get_all(
|
||||
"Asset",
|
||||
filters={"purchase_invoice": self.name, "item_code": item.item_code},
|
||||
filters={
|
||||
"purchase_invoice": self.name,
|
||||
"item_code": item.item_code,
|
||||
"purchase_invoice_item": ("in", [item.name, ""]),
|
||||
},
|
||||
fields=["name", "asset_quantity"],
|
||||
)
|
||||
for asset in assets:
|
||||
|
||||
@@ -2236,6 +2236,62 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
self.assertEqual(pi_expected_values[i][1], gle.debit)
|
||||
self.assertEqual(pi_expected_values[i][2], gle.credit)
|
||||
|
||||
def test_adjust_incoming_rate_from_pi_with_multi_currency(self):
|
||||
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
|
||||
make_landed_cost_voucher,
|
||||
)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
|
||||
|
||||
# Increase the cost of the item
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
qty=10, rate=1, currency="USD", do_not_save=1, supplier="_Test Supplier USD"
|
||||
)
|
||||
pr.conversion_rate = 6300
|
||||
pr.plc_conversion_rate = 1
|
||||
pr.save()
|
||||
pr.submit()
|
||||
|
||||
self.assertEqual(pr.conversion_rate, 6300)
|
||||
self.assertEqual(pr.plc_conversion_rate, 1)
|
||||
self.assertEqual(pr.base_grand_total, 6300 * 10)
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 6300 * 10)
|
||||
|
||||
make_landed_cost_voucher(
|
||||
company=pr.company,
|
||||
receipt_document_type="Purchase Receipt",
|
||||
receipt_document=pr.name,
|
||||
charges=3000,
|
||||
distribute_charges_based_on="Qty",
|
||||
)
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
for row in pi.items:
|
||||
row.rate = 1.1
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 7230 * 10)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -514,7 +514,7 @@ class SalesInvoice(SellingController):
|
||||
)
|
||||
if pos_closing_entry and pos_closing_entry[0]:
|
||||
msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format(
|
||||
frappe.bold("Consolidated Sales Invoice"),
|
||||
frappe.bold(_("Consolidated Sales Invoice")),
|
||||
get_link_to_form("POS Closing Entry", pos_closing_entry[0]),
|
||||
)
|
||||
frappe.throw(msg, title=_("Not Allowed"))
|
||||
@@ -858,7 +858,7 @@ class SalesInvoice(SellingController):
|
||||
|
||||
if account.report_type != "Balance Sheet":
|
||||
msg = (
|
||||
_("Please ensure {} account is a Balance Sheet account.").format(frappe.bold("Debit To"))
|
||||
_("Please ensure {} account is a Balance Sheet account.").format(frappe.bold(_("Debit To")))
|
||||
+ " "
|
||||
)
|
||||
msg += _(
|
||||
@@ -869,7 +869,7 @@ class SalesInvoice(SellingController):
|
||||
if self.customer and account.account_type != "Receivable":
|
||||
msg = (
|
||||
_("Please ensure {} account {} is a Receivable account.").format(
|
||||
frappe.bold("Debit To"), frappe.bold(self.debit_to)
|
||||
frappe.bold(_("Debit To")), frappe.bold(self.debit_to)
|
||||
)
|
||||
+ " "
|
||||
)
|
||||
|
||||
@@ -737,10 +737,7 @@ class Subscription(Document):
|
||||
elif self.generate_invoice_at == "Days before the current subscription period":
|
||||
processing_date = add_days(self.current_invoice_start, -self.number_of_days)
|
||||
|
||||
process_subscription = frappe.new_doc("Process Subscription")
|
||||
process_subscription.posting_date = processing_date
|
||||
process_subscription.subscription = self.name
|
||||
process_subscription.save().submit()
|
||||
self.process(posting_date=processing_date)
|
||||
|
||||
|
||||
def is_prorate() -> int:
|
||||
|
||||
@@ -46,5 +46,11 @@ frappe.query_reports["Asset Depreciations and Balances"] = {
|
||||
options: "Asset",
|
||||
depends_on: "eval: doc.group_by == 'Asset'",
|
||||
},
|
||||
{
|
||||
fieldname: "finance_book",
|
||||
label: __("Finance Book"),
|
||||
fieldtype: "Link",
|
||||
options: "Finance Book",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -69,6 +69,9 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset_category"):
|
||||
condition += " and asset_category = %(asset_category)s"
|
||||
if filters.get("finance_book"):
|
||||
condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
@@ -119,6 +122,7 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
"asset_category": filters.get("asset_category"),
|
||||
"finance_book": filters.get("finance_book"),
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -128,6 +132,10 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset"):
|
||||
condition += " and name = %(asset)s"
|
||||
if filters.get("finance_book"):
|
||||
condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = `tabAsset`.name and ads.finance_book = %(finance_book)s)"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT name,
|
||||
@@ -176,6 +184,7 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
"asset": filters.get("asset"),
|
||||
"finance_book": filters.get("finance_book"),
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -46,4 +46,20 @@ frappe.query_reports["Bank Reconciliation Statement"] = {
|
||||
fieldtype: "Check",
|
||||
},
|
||||
],
|
||||
formatter: function (value, row, column, data, default_formatter, filter) {
|
||||
if (column.fieldname == "payment_entry" && value == "Cheques and Deposits incorrectly cleared") {
|
||||
column.link_onclick =
|
||||
"frappe.query_reports['Bank Reconciliation Statement'].open_utility_report()";
|
||||
}
|
||||
return default_formatter(value, row, column, data);
|
||||
},
|
||||
open_utility_report: function () {
|
||||
frappe.route_options = {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
account: frappe.query_report.get_filter_value("account"),
|
||||
report_date: frappe.query_report.get_filter_value("report_date"),
|
||||
};
|
||||
frappe.open_in_new_tab = true;
|
||||
frappe.set_route("query-report", "Cheques and Deposits Incorrectly cleared");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -154,8 +154,8 @@ def get_payment_entries(filters):
|
||||
select
|
||||
"Payment Entry" as payment_document, name as payment_entry,
|
||||
reference_no, reference_date as ref_date,
|
||||
if(paid_to=%(account)s, received_amount, 0) as debit,
|
||||
if(paid_from=%(account)s, paid_amount, 0) as credit,
|
||||
if(paid_to=%(account)s, received_amount_after_tax, 0) as debit,
|
||||
if(paid_from=%(account)s, paid_amount_after_tax, 0) as credit,
|
||||
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
|
||||
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
|
||||
from `tabPayment Entry`
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Cheques and Deposits Incorrectly cleared"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
reqd: 1,
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
},
|
||||
{
|
||||
fieldname: "account",
|
||||
label: __("Bank Account"),
|
||||
fieldtype: "Link",
|
||||
options: "Account",
|
||||
default: frappe.defaults.get_user_default("Company")
|
||||
? locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]
|
||||
: "",
|
||||
reqd: 1,
|
||||
get_query: function () {
|
||||
var company = frappe.query_report.get_filter_value("company");
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_account_list",
|
||||
filters: [
|
||||
["Account", "account_type", "in", "Bank, Cash"],
|
||||
["Account", "is_group", "=", 0],
|
||||
["Account", "disabled", "=", 0],
|
||||
["Account", "company", "=", company],
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "report_date",
|
||||
label: __("Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.get_today(),
|
||||
reqd: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2024-07-30 17:20:07.570971",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2024-07-30 17:20:07.570971",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Cheques and Deposits Incorrectly cleared",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Payment Entry",
|
||||
"report_name": "Cheques and Deposits Incorrectly cleared",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import CustomFunction
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
columns = get_columns()
|
||||
data = build_data(filters)
|
||||
return columns, data
|
||||
|
||||
|
||||
def build_payment_entry_dict(row: dict) -> dict:
|
||||
row_dict = frappe._dict()
|
||||
row_dict.update(
|
||||
{
|
||||
"payment_document": row.get("doctype"),
|
||||
"payment_entry": row.get("name"),
|
||||
"posting_date": row.get("posting_date"),
|
||||
"clearance_date": row.get("clearance_date"),
|
||||
}
|
||||
)
|
||||
if row.get("payment_type") == "Receive" and row.get("party_type") in ["Customer", "Supplier"]:
|
||||
row_dict.update(
|
||||
{
|
||||
"debit": row.get("amount"),
|
||||
"credit": 0,
|
||||
}
|
||||
)
|
||||
else:
|
||||
row_dict.update(
|
||||
{
|
||||
"debit": 0,
|
||||
"credit": row.get("amount"),
|
||||
}
|
||||
)
|
||||
return row_dict
|
||||
|
||||
|
||||
def build_journal_entry_dict(row: dict) -> dict:
|
||||
row_dict = frappe._dict()
|
||||
row_dict.update(
|
||||
{
|
||||
"payment_document": row.get("doctype"),
|
||||
"payment_entry": row.get("name"),
|
||||
"posting_date": row.get("posting_date"),
|
||||
"clearance_date": row.get("clearance_date"),
|
||||
"debit": row.get("debit_in_account_currency"),
|
||||
"credit": row.get("credit_in_account_currency"),
|
||||
}
|
||||
)
|
||||
return row_dict
|
||||
|
||||
|
||||
def build_data(filters):
|
||||
vouchers = get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filters)
|
||||
data = []
|
||||
for x in vouchers:
|
||||
if x.doctype == "Payment Entry":
|
||||
data.append(build_payment_entry_dict(x))
|
||||
elif x.doctype == "Journal Entry":
|
||||
data.append(build_journal_entry_dict(x))
|
||||
return data
|
||||
|
||||
|
||||
def get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filters):
|
||||
je = qb.DocType("Journal Entry")
|
||||
jea = qb.DocType("Journal Entry Account")
|
||||
doctype_name = ConstantColumn("Journal Entry")
|
||||
|
||||
journals = (
|
||||
qb.from_(je)
|
||||
.inner_join(jea)
|
||||
.on(je.name == jea.parent)
|
||||
.select(
|
||||
doctype_name.as_("doctype"),
|
||||
je.name,
|
||||
jea.debit_in_account_currency,
|
||||
jea.credit_in_account_currency,
|
||||
je.posting_date,
|
||||
je.clearance_date,
|
||||
)
|
||||
.where(
|
||||
je.docstatus.eq(1)
|
||||
& jea.account.eq(filters.account)
|
||||
& je.posting_date.gt(filters.report_date)
|
||||
& je.clearance_date.lte(filters.report_date)
|
||||
& (je.is_opening.isnull() | je.is_opening.eq("No"))
|
||||
)
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
ifelse = CustomFunction("IF", ["condition", "then", "else"])
|
||||
pe = qb.DocType("Payment Entry")
|
||||
doctype_name = ConstantColumn("Payment Entry")
|
||||
payments = (
|
||||
qb.from_(pe)
|
||||
.select(
|
||||
doctype_name.as_("doctype"),
|
||||
pe.name,
|
||||
ifelse(pe.paid_from.eq(filters.account), pe.paid_amount, pe.received_amount).as_("amount"),
|
||||
pe.payment_type,
|
||||
pe.party_type,
|
||||
pe.posting_date,
|
||||
pe.clearance_date,
|
||||
)
|
||||
.where(
|
||||
pe.docstatus.eq(1)
|
||||
& (pe.paid_from.eq(filters.account) | pe.paid_to.eq(filters.account))
|
||||
& pe.posting_date.gt(filters.report_date)
|
||||
& pe.clearance_date.lte(filters.report_date)
|
||||
)
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
return journals + payments
|
||||
|
||||
|
||||
def get_columns():
|
||||
return [
|
||||
{
|
||||
"fieldname": "payment_document",
|
||||
"label": _("Payment Document Type"),
|
||||
"fieldtype": "Data",
|
||||
"width": 220,
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_entry",
|
||||
"label": _("Payment Document"),
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "payment_document",
|
||||
"width": 220,
|
||||
},
|
||||
{
|
||||
"fieldname": "debit",
|
||||
"label": _("Debit"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "account_currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "credit",
|
||||
"label": _("Credit"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "account_currency",
|
||||
"width": 120,
|
||||
},
|
||||
{"fieldname": "posting_date", "label": _("Posting Date"), "fieldtype": "Date", "width": 110},
|
||||
{"fieldname": "clearance_date", "label": _("Clearance Date"), "fieldtype": "Date", "width": 110},
|
||||
]
|
||||
@@ -336,7 +336,7 @@ 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")),
|
||||
_("No {0} Accounts found for this company.").format(frappe.bold(_("Tax Withholding"))),
|
||||
title=_("Accounts Missing Error"),
|
||||
)
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
|
||||
@@ -670,6 +670,11 @@ frappe.ui.form.on("Asset", {
|
||||
if (item.asset_location) {
|
||||
frm.set_value("location", item.asset_location);
|
||||
}
|
||||
if (doctype === "Purchase Receipt") {
|
||||
frm.set_value("purchase_receipt_item", item.name);
|
||||
} else if (doctype === "Purchase Invoice") {
|
||||
frm.set_value("purchase_invoice_item", item.name);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -33,14 +33,16 @@
|
||||
"dimension_col_break",
|
||||
"purchase_details_section",
|
||||
"purchase_receipt",
|
||||
"purchase_receipt_item",
|
||||
"purchase_invoice",
|
||||
"purchase_invoice_item",
|
||||
"purchase_date",
|
||||
"available_for_use_date",
|
||||
"total_asset_cost",
|
||||
"additional_asset_cost",
|
||||
"column_break_23",
|
||||
"gross_purchase_amount",
|
||||
"asset_quantity",
|
||||
"purchase_date",
|
||||
"additional_asset_cost",
|
||||
"total_asset_cost",
|
||||
"section_break_23",
|
||||
"calculate_depreciation",
|
||||
"column_break_33",
|
||||
@@ -536,6 +538,20 @@
|
||||
"fieldname": "opening_number_of_booked_depreciations",
|
||||
"fieldtype": "Int",
|
||||
"label": "Opening Number of Booked Depreciations"
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_receipt_item",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Purchase Receipt Item",
|
||||
"options": "Purchase Receipt Item"
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_invoice_item",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Purchase Invoice Item",
|
||||
"options": "Purchase Invoice Item"
|
||||
}
|
||||
],
|
||||
"idx": 72,
|
||||
@@ -579,7 +595,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2024-08-01 16:39:09.340973",
|
||||
"modified": "2024-08-26 23:28:29.095139",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
||||
@@ -94,7 +94,9 @@ class Asset(AccountsController):
|
||||
purchase_amount: DF.Currency
|
||||
purchase_date: DF.Date | None
|
||||
purchase_invoice: DF.Link | None
|
||||
purchase_invoice_item: DF.Link | None
|
||||
purchase_receipt: DF.Link | None
|
||||
purchase_receipt_item: DF.Link | None
|
||||
split_from: DF.Link | None
|
||||
status: DF.Literal[
|
||||
"Draft",
|
||||
@@ -669,7 +671,7 @@ class Asset(AccountsController):
|
||||
if not fixed_asset_account:
|
||||
frappe.throw(
|
||||
_("Set {0} in asset category {1} for company {2}").format(
|
||||
frappe.bold("Fixed Asset Account"),
|
||||
frappe.bold(_("Fixed Asset Account")),
|
||||
frappe.bold(self.asset_category),
|
||||
frappe.bold(self.company),
|
||||
),
|
||||
@@ -691,12 +693,17 @@ class Asset(AccountsController):
|
||||
return cwip_account
|
||||
|
||||
def make_gl_entries(self):
|
||||
if self.check_asset_capitalization_gl_entries():
|
||||
return
|
||||
|
||||
gl_entries = []
|
||||
|
||||
purchase_document = self.get_purchase_document()
|
||||
fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account()
|
||||
|
||||
if purchase_document and self.purchase_amount and getdate(self.available_for_use_date) <= getdate():
|
||||
if (self.is_composite_asset or (purchase_document and self.purchase_amount)) and getdate(
|
||||
self.available_for_use_date
|
||||
) <= getdate():
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
@@ -733,6 +740,24 @@ class Asset(AccountsController):
|
||||
make_gl_entries(gl_entries)
|
||||
self.db_set("booked_fixed_asset", 1)
|
||||
|
||||
def check_asset_capitalization_gl_entries(self):
|
||||
if self.is_composite_asset:
|
||||
result = frappe.db.get_value(
|
||||
"Asset Capitalization",
|
||||
{"target_asset": self.name, "docstatus": 1},
|
||||
["name", "target_fixed_asset_account"],
|
||||
)
|
||||
|
||||
if result:
|
||||
asset_capitalization, target_fixed_asset_account = result
|
||||
# Check GL entries for the retrieved Asset Capitalization and target fixed asset account
|
||||
return has_gl_entries(
|
||||
"Asset Capitalization", asset_capitalization, target_fixed_asset_account
|
||||
)
|
||||
# return if there are no submitted capitalization for given asset
|
||||
return True
|
||||
return False
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_depreciation_rate(self, args, on_validate=False):
|
||||
if isinstance(args, str):
|
||||
@@ -779,6 +804,22 @@ class Asset(AccountsController):
|
||||
return flt((100 * (1 - depreciation_rate)), float_precision)
|
||||
|
||||
|
||||
def has_gl_entries(doctype, docname, target_account):
|
||||
gl_entry = frappe.qb.DocType("GL Entry")
|
||||
gl_entries = (
|
||||
frappe.qb.from_(gl_entry)
|
||||
.select(gl_entry.account)
|
||||
.where(
|
||||
(gl_entry.voucher_type == doctype)
|
||||
& (gl_entry.voucher_no == docname)
|
||||
& (gl_entry.debit != 0)
|
||||
& (gl_entry.account == target_account)
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
return len(gl_entries) > 0
|
||||
|
||||
|
||||
def update_maintenance_status():
|
||||
assets = frappe.get_all(
|
||||
"Asset", filters={"docstatus": 1, "maintenance_required": 1, "disposal_date": ("is", "not set")}
|
||||
@@ -904,7 +945,7 @@ def transfer_asset(args):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_item_details(item_code, asset_category, gross_purchase_amount):
|
||||
asset_category_doc = frappe.get_doc("Asset Category", asset_category)
|
||||
asset_category_doc = frappe.get_cached_doc("Asset Category", asset_category)
|
||||
books = []
|
||||
for d in asset_category_doc.finance_books:
|
||||
books.append(
|
||||
|
||||
@@ -11,6 +11,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
|
||||
onload() {
|
||||
this.setup_queries();
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
|
||||
}
|
||||
|
||||
refresh() {
|
||||
|
||||
@@ -317,7 +317,16 @@ class AssetCapitalization(StockController):
|
||||
if not self.target_is_fixed_asset and not self.get("asset_items"):
|
||||
frappe.throw(_("Consumed Asset Items is mandatory for Decapitalization"))
|
||||
|
||||
if not (self.get("stock_items") or self.get("asset_items") or self.get("service_items")):
|
||||
if self.capitalization_method == "Create a new composite asset" and not (
|
||||
self.get("stock_items") or self.get("asset_items")
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Consumed Stock Items or Consumed Asset Items are mandatory for creating new composite asset"
|
||||
)
|
||||
)
|
||||
|
||||
elif not (self.get("stock_items") or self.get("asset_items") or self.get("service_items")):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Consumed Stock Items, Consumed Asset Items or Consumed Service Items is mandatory for Capitalization"
|
||||
@@ -460,13 +469,24 @@ class AssetCapitalization(StockController):
|
||||
self.get_gl_entries_for_consumed_asset_items(gl_entries, target_account, target_against, precision)
|
||||
self.get_gl_entries_for_consumed_service_items(gl_entries, target_account, target_against, precision)
|
||||
|
||||
self.get_gl_entries_for_target_item(gl_entries, target_against, precision)
|
||||
self.get_gl_entries_for_target_item(gl_entries, target_account, target_against, precision)
|
||||
|
||||
return gl_entries
|
||||
|
||||
def get_target_account(self):
|
||||
if self.target_is_fixed_asset:
|
||||
return self.target_fixed_asset_account
|
||||
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
|
||||
|
||||
asset_category = frappe.get_cached_value("Asset", self.target_asset, "asset_category")
|
||||
if is_cwip_accounting_enabled(asset_category):
|
||||
target_account = get_asset_category_account(
|
||||
"capital_work_in_progress_account",
|
||||
asset_category=asset_category,
|
||||
company=self.company,
|
||||
)
|
||||
return target_account if target_account else self.target_fixed_asset_account
|
||||
else:
|
||||
return self.target_fixed_asset_account
|
||||
else:
|
||||
return self.warehouse_account[self.target_warehouse]["account"]
|
||||
|
||||
@@ -554,13 +574,13 @@ class AssetCapitalization(StockController):
|
||||
)
|
||||
)
|
||||
|
||||
def get_gl_entries_for_target_item(self, gl_entries, target_against, precision):
|
||||
def get_gl_entries_for_target_item(self, gl_entries, target_account, target_against, precision):
|
||||
if self.target_is_fixed_asset:
|
||||
# Capitalization
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.target_fixed_asset_account,
|
||||
"account": target_account,
|
||||
"against": ", ".join(target_against),
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
||||
"debit": flt(self.total_value, precision),
|
||||
|
||||
@@ -31,6 +31,12 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
def test_capitalization_with_perpetual_inventory(self):
|
||||
company = "_Test Company with perpetual inventory"
|
||||
set_depreciation_settings_in_company(company=company)
|
||||
name = frappe.db.get_value(
|
||||
"Asset Category Account",
|
||||
filters={"parent": "Computers", "company_name": company},
|
||||
fieldname=["name"],
|
||||
)
|
||||
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", "")
|
||||
|
||||
# Variables
|
||||
consumed_asset_value = 100000
|
||||
@@ -187,9 +193,10 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
# Test General Ledger Entries
|
||||
default_expense_account = frappe.db.get_value("Company", company, "default_expense_account")
|
||||
expected_gle = {
|
||||
"_Test Fixed Asset - _TC": 3000,
|
||||
"Expenses Included In Asset Valuation - _TC": -1000,
|
||||
default_expense_account: -2000,
|
||||
"_Test Fixed Asset - _TC": -100000.0,
|
||||
default_expense_account: -2000.0,
|
||||
"CWIP Account - _TC": 103000.0,
|
||||
"Expenses Included In Asset Valuation - _TC": -1000.0,
|
||||
}
|
||||
actual_gle = get_actual_gle_dict(asset_capitalization.name)
|
||||
|
||||
@@ -214,6 +221,12 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
def test_capitalization_with_wip_composite_asset(self):
|
||||
company = "_Test Company with perpetual inventory"
|
||||
set_depreciation_settings_in_company(company=company)
|
||||
name = frappe.db.get_value(
|
||||
"Asset Category Account",
|
||||
filters={"parent": "Computers", "company_name": company},
|
||||
fieldname=["name"],
|
||||
)
|
||||
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", "")
|
||||
|
||||
stock_rate = 1000
|
||||
stock_qty = 2
|
||||
@@ -424,7 +437,7 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
self.assertEqual(target_asset.purchase_amount, total_amount)
|
||||
|
||||
expected_gle = {
|
||||
"_Test Fixed Asset - _TC": 1000.0,
|
||||
"CWIP Account - _TC": 1000.0,
|
||||
"Expenses Included In Asset Valuation - _TC": -1000.0,
|
||||
}
|
||||
|
||||
|
||||
@@ -767,8 +767,12 @@ def get_daily_depr_amount(asset, row, schedule_idx, amount):
|
||||
|
||||
every_year_depr = amount / total_years
|
||||
|
||||
depr_period_start_date = add_days(
|
||||
get_last_day(add_months(row.depreciation_start_date, row.frequency_of_depreciation * -1)), 1
|
||||
)
|
||||
|
||||
year_start_date = add_years(
|
||||
row.depreciation_start_date, (row.frequency_of_depreciation * schedule_idx) // 12
|
||||
depr_period_start_date, ((row.frequency_of_depreciation * schedule_idx) // 12)
|
||||
)
|
||||
year_end_date = add_days(add_years(year_start_date, 1), -1)
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ class Supplier(TransactionBase):
|
||||
elif supp_master_name == "Naming Series":
|
||||
set_name_by_naming_series(self)
|
||||
else:
|
||||
self.name = set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self)
|
||||
set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self)
|
||||
|
||||
def on_update(self):
|
||||
self.create_primary_contact()
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Item-wise Purchase History"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "from_date",
|
||||
reqd: 1,
|
||||
label: __("From Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
||||
},
|
||||
{
|
||||
fieldname: "to_date",
|
||||
reqd: 1,
|
||||
default: frappe.datetime.get_today(),
|
||||
label: __("To Date"),
|
||||
fieldtype: "Date",
|
||||
},
|
||||
{
|
||||
fieldname: "item_group",
|
||||
label: __("Item Group"),
|
||||
fieldtype: "Link",
|
||||
options: "Item Group",
|
||||
},
|
||||
{
|
||||
fieldname: "item_code",
|
||||
label: __("Item"),
|
||||
fieldtype: "Link",
|
||||
options: "Item",
|
||||
get_query: () => {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.item_query",
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "supplier",
|
||||
label: __("Supplier"),
|
||||
fieldtype: "Link",
|
||||
options: "Supplier",
|
||||
},
|
||||
],
|
||||
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
let format_fields = ["received_qty", "billed_amt"];
|
||||
|
||||
if (format_fields.includes(column.fieldname) && data && data[column.fieldname] > 0) {
|
||||
value = "<span style='color:green;'>" + value + "</span>";
|
||||
}
|
||||
return value;
|
||||
},
|
||||
};
|
||||
@@ -1,30 +1,30 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2013-05-03 14:55:53",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2017-02-24 20:08:57.446613",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Item-wise Purchase History",
|
||||
"owner": "Administrator",
|
||||
"query": "select\n po_item.item_code as \"Item Code:Link/Item:120\",\n\tpo_item.item_name as \"Item Name::120\",\n po_item.item_group as \"Item Group:Link/Item Group:120\",\n\tpo_item.description as \"Description::150\",\n\tpo_item.qty as \"Qty:Float:100\",\n\tpo_item.uom as \"UOM:Link/UOM:80\",\n\tpo_item.base_rate as \"Rate:Currency:120\",\n\tpo_item.base_amount as \"Amount:Currency:120\",\n\tpo.name as \"Purchase Order:Link/Purchase Order:120\",\n\tpo.transaction_date as \"Transaction Date:Date:140\",\n\tpo.supplier as \"Supplier:Link/Supplier:130\",\n sup.supplier_name as \"Supplier Name::150\",\n\tpo_item.project as \"Project:Link/Project:130\",\n\tifnull(po_item.received_qty, 0) as \"Received Qty:Float:120\",\n\tpo.company as \"Company:Link/Company:\"\nfrom\n\t`tabPurchase Order` po, `tabPurchase Order Item` po_item, `tabSupplier` sup\nwhere\n\tpo.name = po_item.parent and po.supplier = sup.name and po.docstatus = 1\norder by po.name desc",
|
||||
"ref_doctype": "Purchase Order",
|
||||
"report_name": "Item-wise Purchase History",
|
||||
"report_type": "Query Report",
|
||||
"add_total_row": 1,
|
||||
"creation": "2013-05-03 14:55:53",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 5,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2024-06-19 12:12:15.418799",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Item-wise Purchase History",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Purchase Order",
|
||||
"report_name": "Item-wise Purchase History",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Stock User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Purchase User"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
filters = frappe._dict(filters or {})
|
||||
if filters.from_date > filters.to_date:
|
||||
frappe.throw(_("From Date cannot be greater than To Date"))
|
||||
|
||||
columns = get_columns(filters)
|
||||
data = get_data(filters)
|
||||
|
||||
chart_data = get_chart_data(data)
|
||||
|
||||
return columns, data, None, chart_data
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
return [
|
||||
{
|
||||
"label": _("Item Code"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "item_code",
|
||||
"options": "Item",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Item Name"),
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "item_name",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Item Group"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "item_group",
|
||||
"options": "Item Group",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Description"),
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "description",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Quantity"),
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "quantity",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("UOM"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "uom",
|
||||
"options": "UOM",
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
"label": _("Rate"),
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Amount"),
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Purchase Order"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "purchase_order",
|
||||
"options": "Purchase Order",
|
||||
"width": 160,
|
||||
},
|
||||
{
|
||||
"label": _("Transaction Date"),
|
||||
"fieldtype": "Date",
|
||||
"fieldname": "transaction_date",
|
||||
"width": 110,
|
||||
},
|
||||
{
|
||||
"label": _("Supplier"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "supplier",
|
||||
"options": "Supplier",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Supplier Name"),
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "supplier_name",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Supplier Group"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "supplier_group",
|
||||
"options": "Supplier Group",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Project"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "project",
|
||||
"options": "Project",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Received Quantity"),
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "received_qty",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Billed Amount"),
|
||||
"fieldtype": "Currency",
|
||||
"fieldname": "billed_amt",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Company"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "company",
|
||||
"options": "Company",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Currency"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "currency",
|
||||
"options": "Currency",
|
||||
"hidden": 1,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
data = []
|
||||
|
||||
company_list = get_descendants_of("Company", filters.get("company"))
|
||||
company_list.append(filters.get("company"))
|
||||
|
||||
supplier_details = get_supplier_details()
|
||||
item_details = get_item_details()
|
||||
purchase_order_records = get_purchase_order_details(company_list, filters)
|
||||
|
||||
for record in purchase_order_records:
|
||||
supplier_record = supplier_details.get(record.supplier)
|
||||
item_record = item_details.get(record.item_code)
|
||||
row = {
|
||||
"item_code": record.get("item_code"),
|
||||
"item_name": item_record.get("item_name"),
|
||||
"item_group": item_record.get("item_group"),
|
||||
"description": record.get("description"),
|
||||
"quantity": record.get("qty"),
|
||||
"uom": record.get("uom"),
|
||||
"rate": record.get("base_rate"),
|
||||
"amount": record.get("base_amount"),
|
||||
"purchase_order": record.get("name"),
|
||||
"transaction_date": record.get("transaction_date"),
|
||||
"supplier": record.get("supplier"),
|
||||
"supplier_name": supplier_record.get("supplier_name"),
|
||||
"supplier_group": supplier_record.get("supplier_group"),
|
||||
"project": record.get("project"),
|
||||
"received_qty": flt(record.get("received_qty")),
|
||||
"billed_amt": flt(record.get("billed_amt")),
|
||||
"company": record.get("company"),
|
||||
}
|
||||
row["currency"] = frappe.get_cached_value("Company", row["company"], "default_currency")
|
||||
data.append(row)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_supplier_details():
|
||||
details = frappe.get_all("Supplier", fields=["name", "supplier_name", "supplier_group"])
|
||||
supplier_details = {}
|
||||
for d in details:
|
||||
supplier_details.setdefault(
|
||||
d.name,
|
||||
frappe._dict({"supplier_name": d.supplier_name, "supplier_group": d.supplier_group}),
|
||||
)
|
||||
return supplier_details
|
||||
|
||||
|
||||
def get_item_details():
|
||||
details = frappe.db.get_all("Item", fields=["name", "item_name", "item_group"])
|
||||
item_details = {}
|
||||
for d in details:
|
||||
item_details.setdefault(d.name, frappe._dict({"item_name": d.item_name, "item_group": d.item_group}))
|
||||
return item_details
|
||||
|
||||
|
||||
def get_purchase_order_details(company_list, filters):
|
||||
db_po = frappe.qb.DocType("Purchase Order")
|
||||
db_po_item = frappe.qb.DocType("Purchase Order Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(db_po)
|
||||
.inner_join(db_po_item)
|
||||
.on(db_po_item.parent == db_po.name)
|
||||
.select(
|
||||
db_po.name,
|
||||
db_po.supplier,
|
||||
db_po.transaction_date,
|
||||
db_po.project,
|
||||
db_po.company,
|
||||
db_po_item.item_code,
|
||||
db_po_item.description,
|
||||
db_po_item.qty,
|
||||
db_po_item.uom,
|
||||
db_po_item.base_rate,
|
||||
db_po_item.base_amount,
|
||||
db_po_item.received_qty,
|
||||
(db_po_item.billed_amt * db_po.conversion_rate).as_("billed_amt"),
|
||||
)
|
||||
.where(db_po.docstatus == 1)
|
||||
.where(db_po.company.isin(tuple(company_list)))
|
||||
)
|
||||
|
||||
for field in ("item_code", "item_group"):
|
||||
if filters.get(field):
|
||||
query = query.where(db_po_item[field] == filters[field])
|
||||
|
||||
if filters.get("from_date"):
|
||||
query = query.where(db_po.transaction_date >= filters.from_date)
|
||||
|
||||
if filters.get("to_date"):
|
||||
query = query.where(db_po.transaction_date <= filters.to_date)
|
||||
|
||||
if filters.get("supplier"):
|
||||
query = query.where(db_po.supplier == filters.supplier)
|
||||
|
||||
return query.run(as_dict=1)
|
||||
|
||||
|
||||
def get_chart_data(data):
|
||||
item_wise_purchase_map = {}
|
||||
labels, datapoints = [], []
|
||||
|
||||
for row in data:
|
||||
item_key = row.get("item_code")
|
||||
|
||||
if item_key not in item_wise_purchase_map:
|
||||
item_wise_purchase_map[item_key] = 0
|
||||
|
||||
item_wise_purchase_map[item_key] = flt(item_wise_purchase_map[item_key]) + flt(row.get("amount"))
|
||||
|
||||
item_wise_purchase_map = {
|
||||
item: value
|
||||
for item, value in (sorted(item_wise_purchase_map.items(), key=lambda i: i[1], reverse=True))
|
||||
}
|
||||
|
||||
for key in item_wise_purchase_map:
|
||||
labels.append(key)
|
||||
datapoints.append(item_wise_purchase_map[key])
|
||||
|
||||
return {
|
||||
"data": {
|
||||
"labels": labels[:30], # show max of 30 items in chart
|
||||
"datasets": [{"name": _("Total Purchase Amount"), "values": datapoints[:30]}],
|
||||
},
|
||||
"type": "bar",
|
||||
"fieldtype": "Currency",
|
||||
}
|
||||
@@ -40,6 +40,7 @@ def get_data(filters):
|
||||
po = frappe.qb.DocType("Purchase Order")
|
||||
po_item = frappe.qb.DocType("Purchase Order Item")
|
||||
pi_item = frappe.qb.DocType("Purchase Invoice Item")
|
||||
pr_item = frappe.qb.DocType("Purchase Receipt Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(po)
|
||||
@@ -47,6 +48,8 @@ def get_data(filters):
|
||||
.on(po_item.parent == po.name)
|
||||
.left_join(pi_item)
|
||||
.on((pi_item.po_detail == po_item.name) & (pi_item.docstatus == 1))
|
||||
.left_join(pr_item)
|
||||
.on((pr_item.purchase_order_item == po_item.name) & (pr_item.docstatus == 1))
|
||||
.select(
|
||||
po.transaction_date.as_("date"),
|
||||
po_item.schedule_date.as_("required_date"),
|
||||
@@ -60,7 +63,7 @@ def get_data(filters):
|
||||
(po_item.qty - po_item.received_qty).as_("pending_qty"),
|
||||
Sum(IfNull(pi_item.qty, 0)).as_("billed_qty"),
|
||||
po_item.base_amount.as_("amount"),
|
||||
(po_item.received_qty * po_item.base_rate).as_("received_qty_amount"),
|
||||
(pr_item.base_amount).as_("received_qty_amount"),
|
||||
(po_item.billed_amt * IfNull(po.conversion_rate, 1)).as_("billed_amount"),
|
||||
(po_item.base_amount - (po_item.billed_amt * IfNull(po.conversion_rate, 1))).as_(
|
||||
"pending_amount"
|
||||
|
||||
@@ -233,7 +233,7 @@ class AccountsController(TransactionBase):
|
||||
).format(
|
||||
frappe.bold(document_type),
|
||||
get_link_to_form(self.doctype, self.get("return_against")),
|
||||
frappe.bold("Update Outstanding for Self"),
|
||||
frappe.bold(_("Update Outstanding for Self")),
|
||||
get_link_to_form("Payment Reconciliation", "Payment Reconciliation"),
|
||||
)
|
||||
)
|
||||
@@ -1334,6 +1334,12 @@ class AccountsController(TransactionBase):
|
||||
# Cancelling existing exchange gain/loss journals is handled during the `on_cancel` event.
|
||||
# see accounts/utils.py:cancel_exchange_gain_loss_journal()
|
||||
if self.docstatus == 1:
|
||||
if dimensions_dict is None:
|
||||
dimensions_dict = frappe._dict()
|
||||
active_dimensions = get_dimensions()[0]
|
||||
for dim in active_dimensions:
|
||||
dimensions_dict[dim.fieldname] = self.get(dim.fieldname)
|
||||
|
||||
if self.get("doctype") == "Journal Entry":
|
||||
# 'args' is populated with exchange gain/loss account and the amount to be booked.
|
||||
# These are generated by Sales/Purchase Invoice during reconciliation and advance allocation.
|
||||
@@ -1962,7 +1968,9 @@ class AccountsController(TransactionBase):
|
||||
|
||||
def raise_missing_debit_credit_account_error(self, party_type, party):
|
||||
"""Raise an error if debit to/credit to account does not exist."""
|
||||
db_or_cr = frappe.bold("Debit To") if self.doctype == "Sales Invoice" else frappe.bold("Credit To")
|
||||
db_or_cr = (
|
||||
frappe.bold(_("Debit To")) if self.doctype == "Sales Invoice" else frappe.bold(_("Credit To"))
|
||||
)
|
||||
rec_or_pay = "Receivable" if self.doctype == "Sales Invoice" else "Payable"
|
||||
|
||||
link_to_party = frappe.utils.get_link_to_form(party_type, party)
|
||||
@@ -3085,9 +3093,9 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child
|
||||
child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
|
||||
if not child_item.warehouse:
|
||||
frappe.throw(
|
||||
_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.").format(
|
||||
frappe.bold("default warehouse"), frappe.bold(item.item_code)
|
||||
)
|
||||
_(
|
||||
"Cannot find a default warehouse for item {0}. Please set one in the Item Master or in Stock Settings."
|
||||
).format(frappe.bold(item.item_code))
|
||||
)
|
||||
|
||||
set_child_tax_template_and_map(item, child_item, p_doc)
|
||||
|
||||
@@ -689,9 +689,11 @@ class BuyingController(SubcontractingController):
|
||||
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
self.process_fixed_asset()
|
||||
|
||||
if self.doctype in ["Purchase Order", "Purchase Receipt"] and not frappe.db.get_single_value(
|
||||
"Buying Settings", "disable_last_purchase_rate"
|
||||
):
|
||||
if self.doctype in [
|
||||
"Purchase Order",
|
||||
"Purchase Receipt",
|
||||
"Purchase Invoice",
|
||||
] and not frappe.db.get_single_value("Buying Settings", "disable_last_purchase_rate"):
|
||||
update_last_purchase_rate(self, is_submit=1)
|
||||
|
||||
def on_cancel(self):
|
||||
@@ -820,6 +822,8 @@ class BuyingController(SubcontractingController):
|
||||
"asset_quantity": asset_quantity,
|
||||
"purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None,
|
||||
"purchase_invoice": self.name if self.doctype == "Purchase Invoice" else None,
|
||||
"purchase_receipt_item": row.name if self.doctype == "Purchase Receipt" else None,
|
||||
"purchase_invoice_item": row.name if self.doctype == "Purchase Invoice" else None,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ def validate_item_attribute_value(attributes_list, attribute, attribute_value, i
|
||||
)
|
||||
msg += "<br>" + _(
|
||||
"To still proceed with editing this Attribute Value, enable {0} in Item Variant Settings."
|
||||
).format(frappe.bold("Allow Rename Attribute Value"))
|
||||
).format(frappe.bold(_("Allow Rename Attribute Value")))
|
||||
|
||||
frappe.throw(msg, InvalidItemAttributeValueError, title=_("Edit Not Allowed"))
|
||||
|
||||
|
||||
@@ -366,7 +366,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
|
||||
def get_empty_batches(filters, start, page_len, filtered_batches=None, txt=None):
|
||||
query_filter = {"item": filters.get("item_code")}
|
||||
query_filter = {"item": filters.get("item_code"), "disabled": 0}
|
||||
if txt:
|
||||
query_filter["name"] = ("like", f"%{txt}%")
|
||||
|
||||
|
||||
@@ -697,7 +697,7 @@ class SellingController(StockController):
|
||||
duplicate_items_msg = _("Item {0} entered multiple times.").format(frappe.bold(d.item_code))
|
||||
duplicate_items_msg += "<br><br>"
|
||||
duplicate_items_msg += _("Please enable {} in {} to allow same item in multiple rows").format(
|
||||
frappe.bold("Allow Item to Be Added Multiple Times in a Transaction"),
|
||||
frappe.bold(_("Allow Item to Be Added Multiple Times in a Transaction")),
|
||||
get_link_to_form("Selling Settings", "Selling Settings"),
|
||||
)
|
||||
if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1:
|
||||
|
||||
@@ -185,7 +185,8 @@
|
||||
{
|
||||
"fieldname": "expected_closing",
|
||||
"fieldtype": "Date",
|
||||
"label": "Expected Closing Date"
|
||||
"label": "Expected Closing Date",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_14",
|
||||
@@ -357,6 +358,7 @@
|
||||
"fieldname": "transaction_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Opportunity Date",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "transaction_date",
|
||||
"oldfieldtype": "Date",
|
||||
"reqd": 1,
|
||||
@@ -388,6 +390,7 @@
|
||||
"fieldname": "first_response_time",
|
||||
"fieldtype": "Duration",
|
||||
"label": "First Response Time",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -622,7 +625,7 @@
|
||||
"icon": "fa fa-info-sign",
|
||||
"idx": 195,
|
||||
"links": [],
|
||||
"modified": "2022-10-13 12:42:21.545636",
|
||||
"modified": "2024-08-20 04:12:29.095761",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Opportunity",
|
||||
|
||||
@@ -27,6 +27,29 @@ class TestProspect(unittest.TestCase):
|
||||
address_doc.reload()
|
||||
self.assertEqual(address_doc.has_link("Prospect", prospect_doc.name), True)
|
||||
|
||||
def test_make_customer_from_prospect(self):
|
||||
from erpnext.crm.doctype.prospect.prospect import make_customer as make_customer_from_prospect
|
||||
|
||||
frappe.delete_doc_if_exists("Customer", "_Test Prospect")
|
||||
|
||||
prospect = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Prospect",
|
||||
"company_name": "_Test Prospect",
|
||||
"customer_group": "_Test Customer Group",
|
||||
}
|
||||
)
|
||||
prospect.insert()
|
||||
|
||||
customer = make_customer_from_prospect("_Test Prospect")
|
||||
|
||||
self.assertEqual(customer.doctype, "Customer")
|
||||
self.assertEqual(customer.company_name, "_Test Prospect")
|
||||
self.assertEqual(customer.customer_group, "_Test Customer Group")
|
||||
|
||||
customer.company = "_Test Company"
|
||||
customer.insert()
|
||||
|
||||
|
||||
def make_prospect(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -96,7 +96,7 @@ def add_bank_accounts(response, bank, company):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please setup and enable a group account with the Account Type - {0} for the company {1}"
|
||||
).format(frappe.bold("Bank"), company)
|
||||
).format(frappe.bold(_("Bank")), company)
|
||||
)
|
||||
|
||||
for account in response["accounts"]:
|
||||
|
||||
@@ -10,7 +10,17 @@ source_link = "https://github.com/frappe/erpnext"
|
||||
app_logo_url = "/assets/erpnext/images/erpnext-logo.svg"
|
||||
|
||||
|
||||
develop_version = "14.x.x-develop"
|
||||
add_to_apps_screen = [
|
||||
{
|
||||
"name": "erpnext",
|
||||
"logo": "/assets/erpnext/images/erpnext-logo-blue.png",
|
||||
"title": "ERPNext",
|
||||
"route": "/app/home",
|
||||
"has_permission": "erpnext.check_app_permission",
|
||||
}
|
||||
]
|
||||
|
||||
develop_version = "15.x.x-develop"
|
||||
|
||||
app_include_js = "erpnext.bundle.js"
|
||||
app_include_css = "erpnext.bundle.css"
|
||||
|
||||
@@ -5,18 +5,17 @@
|
||||
"document_type": "Document",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"bom_and_work_order_tab",
|
||||
"raw_materials_consumption_section",
|
||||
"material_consumption",
|
||||
"get_rm_cost_from_consumption_entry",
|
||||
"column_break_3",
|
||||
"backflush_raw_materials_based_on",
|
||||
"capacity_planning",
|
||||
"disable_capacity_planning",
|
||||
"allow_overtime",
|
||||
"allow_production_on_holidays",
|
||||
"column_break_5",
|
||||
"capacity_planning_for_days",
|
||||
"mins_between_operations",
|
||||
"validate_components_quantities_per_bom",
|
||||
"bom_section",
|
||||
"update_bom_costs_automatically",
|
||||
"column_break_lhyt",
|
||||
"manufacture_sub_assembly_in_operation",
|
||||
"section_break_6",
|
||||
"default_wip_warehouse",
|
||||
"default_fg_warehouse",
|
||||
@@ -30,8 +29,14 @@
|
||||
"add_corrective_operation_cost_in_finished_good_valuation",
|
||||
"column_break_24",
|
||||
"job_card_excess_transfer",
|
||||
"capacity_planning",
|
||||
"disable_capacity_planning",
|
||||
"allow_overtime",
|
||||
"allow_production_on_holidays",
|
||||
"column_break_5",
|
||||
"capacity_planning_for_days",
|
||||
"mins_between_operations",
|
||||
"other_settings_section",
|
||||
"update_bom_costs_automatically",
|
||||
"set_op_cost_and_scrape_from_sub_assemblies",
|
||||
"column_break_23",
|
||||
"make_serial_no_batch_from_work_order"
|
||||
@@ -149,7 +154,7 @@
|
||||
{
|
||||
"fieldname": "raw_materials_consumption_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Raw Materials Consumption"
|
||||
"label": "Raw Materials Consumption "
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_16",
|
||||
@@ -183,8 +188,8 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "job_card_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Job Card"
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Job Card and Capacity Planning"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_24",
|
||||
@@ -210,13 +215,41 @@
|
||||
"fieldname": "get_rm_cost_from_consumption_entry",
|
||||
"fieldtype": "Check",
|
||||
"label": "Get Raw Materials Cost from Consumption Entry"
|
||||
},
|
||||
{
|
||||
"fieldname": "bom_and_work_order_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "BOM and Production"
|
||||
},
|
||||
{
|
||||
"fieldname": "bom_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "BOM"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_lhyt",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If enabled then system will manufacture Sub-assembly against the Job Card (operation).",
|
||||
"fieldname": "manufacture_sub_assembly_in_operation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Manufacture Sub-assembly in Operation"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.backflush_raw_materials_based_on == \"BOM\"",
|
||||
"fieldname": "validate_components_quantities_per_bom",
|
||||
"fieldtype": "Check",
|
||||
"label": "Validate Components Quantities Per BOM"
|
||||
}
|
||||
],
|
||||
"icon": "icon-wrench",
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-08 19:00:37.561244",
|
||||
"modified": "2024-09-02 12:12:03.132567",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Manufacturing Settings",
|
||||
@@ -234,4 +267,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,15 +29,22 @@ class ManufacturingSettings(Document):
|
||||
get_rm_cost_from_consumption_entry: DF.Check
|
||||
job_card_excess_transfer: DF.Check
|
||||
make_serial_no_batch_from_work_order: DF.Check
|
||||
manufacture_sub_assembly_in_operation: DF.Check
|
||||
material_consumption: DF.Check
|
||||
mins_between_operations: DF.Int
|
||||
overproduction_percentage_for_sales_order: DF.Percent
|
||||
overproduction_percentage_for_work_order: DF.Percent
|
||||
set_op_cost_and_scrape_from_sub_assemblies: DF.Check
|
||||
update_bom_costs_automatically: DF.Check
|
||||
validate_components_quantities_per_bom: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
def before_save(self):
|
||||
self.reset_values()
|
||||
|
||||
def reset_values(self):
|
||||
if self.backflush_raw_materials_based_on != "BOM" and self.validate_components_quantities_per_bom:
|
||||
self.validate_components_quantities_per_bom = 0
|
||||
|
||||
|
||||
def get_mins_between_operations():
|
||||
|
||||
@@ -2053,6 +2053,108 @@ class TestWorkOrder(FrappeTestCase):
|
||||
"BOM",
|
||||
)
|
||||
|
||||
def test_disassemby_order(self):
|
||||
fg_item = "Test Disassembly Item"
|
||||
source_warehouse = "Stores - _TC"
|
||||
raw_materials = ["Test Disassembly RM Item 1", "Test Disassembly RM Item 2"]
|
||||
|
||||
make_item(fg_item, {"is_stock_item": 1})
|
||||
for item in raw_materials:
|
||||
make_item(item, {"is_stock_item": 1})
|
||||
test_stock_entry.make_stock_entry(
|
||||
item_code=item,
|
||||
target=source_warehouse,
|
||||
qty=1,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
make_bom(item=fg_item, source_warehouse=source_warehouse, raw_materials=raw_materials)
|
||||
|
||||
wo = make_wo_order_test_record(
|
||||
item=fg_item,
|
||||
qty=1,
|
||||
source_warehouse=source_warehouse,
|
||||
skip_transfer=1,
|
||||
)
|
||||
|
||||
stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 1))
|
||||
for row in stock_entry.items:
|
||||
if row.item_code in raw_materials:
|
||||
row.s_warehouse = source_warehouse
|
||||
|
||||
stock_entry.submit()
|
||||
|
||||
wo.reload()
|
||||
self.assertEqual(wo.status, "Completed")
|
||||
|
||||
stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Disassemble", 1))
|
||||
stock_entry.save()
|
||||
|
||||
self.assertEqual(stock_entry.purpose, "Disassemble")
|
||||
|
||||
for row in stock_entry.items:
|
||||
if row.item_code == fg_item:
|
||||
self.assertTrue(row.s_warehouse)
|
||||
self.assertFalse(row.t_warehouse)
|
||||
else:
|
||||
self.assertFalse(row.s_warehouse)
|
||||
self.assertTrue(row.t_warehouse)
|
||||
|
||||
stock_entry.submit()
|
||||
|
||||
def test_components_qty_for_bom_based_manufacture_entry(self):
|
||||
frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM")
|
||||
frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 1)
|
||||
|
||||
fg_item = "Test FG Item For Component Validation"
|
||||
source_warehouse = "Stores - _TC"
|
||||
raw_materials = ["Test Component Validation RM Item 1", "Test Component Validation RM Item 2"]
|
||||
|
||||
make_item(fg_item, {"is_stock_item": 1})
|
||||
for item in raw_materials:
|
||||
make_item(item, {"is_stock_item": 1})
|
||||
test_stock_entry.make_stock_entry(
|
||||
item_code=item,
|
||||
target=source_warehouse,
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
make_bom(item=fg_item, source_warehouse=source_warehouse, raw_materials=raw_materials)
|
||||
|
||||
wo = make_wo_order_test_record(
|
||||
item=fg_item,
|
||||
qty=10,
|
||||
source_warehouse=source_warehouse,
|
||||
)
|
||||
|
||||
transfer_entry = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 10))
|
||||
transfer_entry.save()
|
||||
for row in transfer_entry.items:
|
||||
row.qty = 5
|
||||
|
||||
self.assertRaises(frappe.ValidationError, transfer_entry.save)
|
||||
|
||||
transfer_entry.reload()
|
||||
for row in transfer_entry.items:
|
||||
self.assertEqual(row.qty, 10)
|
||||
|
||||
transfer_entry.submit()
|
||||
|
||||
manufacture_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 10))
|
||||
manufacture_entry.save()
|
||||
for row in manufacture_entry.items:
|
||||
if not row.s_warehouse:
|
||||
continue
|
||||
|
||||
row.qty = 5
|
||||
|
||||
self.assertRaises(frappe.ValidationError, manufacture_entry.save)
|
||||
manufacture_entry.reload()
|
||||
manufacture_entry.submit()
|
||||
|
||||
frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 0)
|
||||
|
||||
|
||||
def make_operation(**kwargs):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
@@ -2370,6 +2472,7 @@ def make_wo_order_test_record(**args):
|
||||
wo_order.batch_size = args.batch_size or 0
|
||||
|
||||
if args.source_warehouse:
|
||||
wo_order.source_warehouse = args.source_warehouse
|
||||
for item in wo_order.get("required_items"):
|
||||
item.source_warehouse = args.source_warehouse
|
||||
|
||||
|
||||
@@ -183,13 +183,30 @@ frappe.ui.form.on("Work Order", {
|
||||
}
|
||||
}
|
||||
|
||||
if (frm.doc.status == "Completed") {
|
||||
if (frm.doc.__onload.backflush_raw_materials_based_on == "Material Transferred for Manufacture") {
|
||||
frm.add_custom_button(
|
||||
__("BOM"),
|
||||
() => {
|
||||
frm.trigger("make_bom");
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
frm.doc.status == "Completed" &&
|
||||
frm.doc.__onload.backflush_raw_materials_based_on == "Material Transferred for Manufacture"
|
||||
frm.doc.docstatus === 1 &&
|
||||
["Closed", "Completed"].includes(frm.doc.status) &&
|
||||
frm.doc.produced_qty > 0
|
||||
) {
|
||||
frm.add_custom_button(__("Create BOM"), () => {
|
||||
frm.trigger("make_bom");
|
||||
});
|
||||
frm.add_custom_button(
|
||||
__("Disassembly Order"),
|
||||
() => {
|
||||
frm.trigger("make_disassembly_order");
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
|
||||
frm.trigger("add_custom_button_to_return_components");
|
||||
@@ -337,6 +354,23 @@ frappe.ui.form.on("Work Order", {
|
||||
});
|
||||
},
|
||||
|
||||
make_disassembly_order(frm) {
|
||||
erpnext.work_order
|
||||
.show_prompt_for_qty_input(frm, "Disassemble")
|
||||
.then((data) => {
|
||||
return frappe.xcall("erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", {
|
||||
work_order_id: frm.doc.name,
|
||||
purpose: "Disassemble",
|
||||
qty: data.qty,
|
||||
target_warehouse: data.target_warehouse,
|
||||
});
|
||||
})
|
||||
.then((stock_entry) => {
|
||||
frappe.model.sync(stock_entry);
|
||||
frappe.set_route("Form", stock_entry.doctype, stock_entry.name);
|
||||
});
|
||||
},
|
||||
|
||||
show_progress_for_items: function (frm) {
|
||||
var bars = [];
|
||||
var message = "";
|
||||
@@ -745,6 +779,10 @@ erpnext.work_order = {
|
||||
|
||||
get_max_transferable_qty: (frm, purpose) => {
|
||||
let max = 0;
|
||||
if (purpose === "Disassemble") {
|
||||
return flt(frm.doc.produced_qty);
|
||||
}
|
||||
|
||||
if (frm.doc.skip_transfer) {
|
||||
max = flt(frm.doc.qty) - flt(frm.doc.produced_qty);
|
||||
} else {
|
||||
@@ -759,15 +797,38 @@ erpnext.work_order = {
|
||||
|
||||
show_prompt_for_qty_input: function (frm, purpose) {
|
||||
let max = this.get_max_transferable_qty(frm, purpose);
|
||||
|
||||
let fields = [
|
||||
{
|
||||
fieldtype: "Float",
|
||||
label: __("Qty for {0}", [__(purpose)]),
|
||||
fieldname: "qty",
|
||||
description: __("Max: {0}", [max]),
|
||||
default: max,
|
||||
},
|
||||
];
|
||||
|
||||
if (purpose === "Disassemble") {
|
||||
fields.push({
|
||||
fieldtype: "Link",
|
||||
options: "Warehouse",
|
||||
fieldname: "target_warehouse",
|
||||
label: __("Target Warehouse"),
|
||||
default: frm.doc.source_warehouse || frm.doc.wip_warehouse,
|
||||
get_query() {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
frappe.prompt(
|
||||
{
|
||||
fieldtype: "Float",
|
||||
label: __("Qty for {0}", [__(purpose)]),
|
||||
fieldname: "qty",
|
||||
description: __("Max: {0}", [max]),
|
||||
default: max,
|
||||
},
|
||||
fields,
|
||||
(data) => {
|
||||
max += (frm.doc.qty * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100;
|
||||
|
||||
|
||||
@@ -1359,7 +1359,7 @@ def set_work_order_ops(name):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_stock_entry(work_order_id, purpose, qty=None):
|
||||
def make_stock_entry(work_order_id, purpose, qty=None, target_warehouse=None):
|
||||
work_order = frappe.get_doc("Work Order", work_order_id)
|
||||
if not frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group"):
|
||||
wip_warehouse = work_order.wip_warehouse
|
||||
@@ -1389,9 +1389,16 @@ def make_stock_entry(work_order_id, purpose, qty=None):
|
||||
stock_entry.to_warehouse = work_order.fg_warehouse
|
||||
stock_entry.project = work_order.project
|
||||
|
||||
if purpose == "Disassemble":
|
||||
stock_entry.from_warehouse = work_order.fg_warehouse
|
||||
stock_entry.to_warehouse = target_warehouse or work_order.source_warehouse
|
||||
|
||||
stock_entry.set_stock_entry_type()
|
||||
stock_entry.get_items()
|
||||
stock_entry.set_serial_no_batch_for_finished_good()
|
||||
|
||||
if purpose != "Disassemble":
|
||||
stock_entry.set_serial_no_batch_for_finished_good()
|
||||
|
||||
return stock_entry.as_dict()
|
||||
|
||||
|
||||
|
||||
@@ -371,3 +371,7 @@ erpnext.patches.v15_0.update_warehouse_field_in_asset_repair_consumed_item_docty
|
||||
erpnext.patches.v15_0.update_asset_repair_field_in_stock_entry
|
||||
erpnext.patches.v15_0.update_total_number_of_booked_depreciations
|
||||
erpnext.patches.v15_0.do_not_use_batchwise_valuation
|
||||
erpnext.patches.v15_0.drop_index_posting_datetime_from_sle
|
||||
erpnext.patches.v15_0.add_disassembly_order_stock_entry_type #1
|
||||
erpnext.patches.v15_0.set_standard_stock_entry_type
|
||||
erpnext.patches.v15_0.link_purchase_item_to_asset_doc
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
if not frappe.db.exists("Stock Entry Type", "Disassemble"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Stock Entry Type",
|
||||
"name": "Disassemble",
|
||||
"purpose": "Disassemble",
|
||||
"is_standard": 1,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
@@ -0,0 +1,16 @@
|
||||
import click
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
table = "tabStock Ledger Entry"
|
||||
index = "posting_datetime_creation_index"
|
||||
|
||||
if not frappe.db.has_index(table, index):
|
||||
return
|
||||
|
||||
try:
|
||||
frappe.db.sql_ddl(f"ALTER TABLE `{table}` DROP INDEX `{index}`")
|
||||
click.echo(f"✓ dropped {index} index from {table}")
|
||||
except Exception:
|
||||
frappe.log_error("Failed to drop index")
|
||||
74
erpnext/patches/v15_0/link_purchase_item_to_asset_doc.py
Normal file
74
erpnext/patches/v15_0/link_purchase_item_to_asset_doc.py
Normal file
@@ -0,0 +1,74 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
if frappe.db.has_column("Asset", "purchase_invoice_item") and frappe.db.has_column(
|
||||
"Asset", "purchase_receipt_item"
|
||||
):
|
||||
# Get all assets with their related Purchase Invoice and Purchase Receipt
|
||||
assets = frappe.get_all(
|
||||
"Asset",
|
||||
filters={"docstatus": 0},
|
||||
fields=[
|
||||
"name",
|
||||
"item_code",
|
||||
"purchase_invoice",
|
||||
"purchase_receipt",
|
||||
"gross_purchase_amount",
|
||||
"asset_quantity",
|
||||
"purchase_invoice_item",
|
||||
"purchase_receipt_item",
|
||||
],
|
||||
)
|
||||
|
||||
for asset in assets:
|
||||
# Get Purchase Invoice Items
|
||||
if asset.purchase_invoice and not asset.purchase_invoice_item:
|
||||
purchase_invoice_item = get_linked_item(
|
||||
"Purchase Invoice Item",
|
||||
asset.purchase_invoice,
|
||||
asset.item_code,
|
||||
asset.gross_purchase_amount,
|
||||
asset.asset_quantity,
|
||||
)
|
||||
frappe.db.set_value("Asset", asset.name, "purchase_invoice_item", purchase_invoice_item)
|
||||
|
||||
# Get Purchase Receipt Items
|
||||
if asset.purchase_receipt and not asset.purchase_receipt_item:
|
||||
purchase_receipt_item = get_linked_item(
|
||||
"Purchase Receipt Item",
|
||||
asset.purchase_receipt,
|
||||
asset.item_code,
|
||||
asset.gross_purchase_amount,
|
||||
asset.asset_quantity,
|
||||
)
|
||||
frappe.db.set_value("Asset", asset.name, "purchase_receipt_item", purchase_receipt_item)
|
||||
|
||||
|
||||
def get_linked_item(doctype, parent, item_code, amount, quantity):
|
||||
items = frappe.get_all(
|
||||
doctype,
|
||||
filters={
|
||||
"parenttype": doctype.replace(" Item", ""),
|
||||
"parent": parent,
|
||||
"item_code": item_code,
|
||||
},
|
||||
fields=["name", "rate", "amount", "qty", "landed_cost_voucher_amount"],
|
||||
)
|
||||
if len(items) == 1:
|
||||
# If only one item exists, return it directly
|
||||
return items[0].name
|
||||
|
||||
for item in items:
|
||||
landed_cost = item.get("landed_cost_voucher_amount", 0)
|
||||
# Check if the asset is grouped
|
||||
if quantity > 1:
|
||||
if item.amount + landed_cost == amount and item.qty == quantity:
|
||||
return item.name
|
||||
elif item.qty == quantity:
|
||||
return item.name
|
||||
else:
|
||||
if item.rate + (landed_cost / item.qty) == amount:
|
||||
return item.name
|
||||
|
||||
return items[0].name if items else None
|
||||
17
erpnext/patches/v15_0/set_standard_stock_entry_type.py
Normal file
17
erpnext/patches/v15_0/set_standard_stock_entry_type.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
for stock_entry_type in [
|
||||
"Material Issue",
|
||||
"Material Receipt",
|
||||
"Material Transfer",
|
||||
"Material Transfer for Manufacture",
|
||||
"Material Consumption for Manufacture",
|
||||
"Manufacture",
|
||||
"Repack",
|
||||
"Send to Subcontractor",
|
||||
"Disassemble",
|
||||
]:
|
||||
if frappe.db.exists("Stock Entry Type", stock_entry_type):
|
||||
frappe.db.set_value("Stock Entry Type", stock_entry_type, "is_standard", 1)
|
||||
BIN
erpnext/public/images/erpnext-logo-blue.png
Normal file
BIN
erpnext/public/images/erpnext-logo-blue.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
@@ -374,6 +374,14 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
label: "Cost Center",
|
||||
options: "Cost Center",
|
||||
depends_on: "eval:doc.action=='Create Voucher' && doc.document_type=='Payment Entry'",
|
||||
get_query: () => {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
company: this.company,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
|
||||
@@ -1421,12 +1421,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
let show = cint(this.frm.doc.discount_amount) ||
|
||||
((this.frm.doc.taxes || []).filter(function(d) {return d.included_in_print_rate===1}).length);
|
||||
|
||||
if(frappe.meta.get_docfield(cur_frm.doctype, "net_total"))
|
||||
if(this.frm.doc.doctype && frappe.meta.get_docfield(this.frm.doc.doctype, "net_total")) {
|
||||
this.frm.toggle_display("net_total", show);
|
||||
}
|
||||
|
||||
if(frappe.meta.get_docfield(cur_frm.doctype, "base_net_total"))
|
||||
if(this.frm.doc.doctype && frappe.meta.get_docfield(this.frm.doc.doctype, "base_net_total")) {
|
||||
this.frm.toggle_display("base_net_total", (show && (me.frm.doc.currency != company_currency)));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
change_grid_labels(company_currency) {
|
||||
|
||||
@@ -116,14 +116,17 @@ erpnext.financial_statements = {
|
||||
erpnext.financial_statements.filters = get_filters();
|
||||
|
||||
let fiscal_year = erpnext.utils.get_fiscal_year(frappe.datetime.get_today());
|
||||
var filters = report.get_values();
|
||||
|
||||
frappe.model.with_doc("Fiscal Year", fiscal_year, function (r) {
|
||||
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
|
||||
frappe.query_report.set_filter_value({
|
||||
period_start_date: fy.year_start_date,
|
||||
period_end_date: fy.year_end_date,
|
||||
if (!filters.period_start_date || !filters.period_end_date) {
|
||||
frappe.model.with_doc("Fiscal Year", fiscal_year, function (r) {
|
||||
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
|
||||
frappe.query_report.set_filter_value({
|
||||
period_start_date: fy.year_start_date,
|
||||
period_end_date: fy.year_end_date,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (report.page) {
|
||||
const views_menu = report.page.add_custom_button_group(__("Financial Statements"));
|
||||
|
||||
@@ -368,8 +368,28 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
];
|
||||
}
|
||||
|
||||
get_batch_qty(batch_no, callback) {
|
||||
let warehouse = this.item.s_warehouse || this.item.t_warehouse || this.item.warehouse;
|
||||
frappe.call({
|
||||
method: "erpnext.stock.doctype.batch.batch.get_batch_qty",
|
||||
args: {
|
||||
batch_no: batch_no,
|
||||
warehouse: warehouse,
|
||||
item_code: this.item.item_code,
|
||||
posting_date: this.frm.doc.posting_date,
|
||||
posting_time: this.frm.doc.posting_time,
|
||||
},
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
callback(flt(r.message));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
get_dialog_table_fields() {
|
||||
let fields = [];
|
||||
let me = this;
|
||||
|
||||
if (this.item.has_serial_no) {
|
||||
fields.push({
|
||||
@@ -395,6 +415,15 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
fieldname: "batch_no",
|
||||
label: __("Batch No"),
|
||||
in_list_view: 1,
|
||||
change() {
|
||||
let doc = this.doc;
|
||||
if (!doc.qty && me.item.type_of_transaction === "Outward") {
|
||||
me.get_batch_qty(doc.batch_no, (qty) => {
|
||||
doc.qty = qty;
|
||||
this.grid.set_value("qty", qty, doc);
|
||||
});
|
||||
}
|
||||
},
|
||||
get_query: () => {
|
||||
let is_inward = false;
|
||||
if (
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"gender",
|
||||
"lead_name",
|
||||
"opportunity_name",
|
||||
"prospect_name",
|
||||
"account_manager",
|
||||
"image",
|
||||
"defaults_tab",
|
||||
@@ -307,13 +308,15 @@
|
||||
"fetch_from": "customer_primary_contact.mobile_no",
|
||||
"fieldname": "mobile_no",
|
||||
"fieldtype": "Read Only",
|
||||
"label": "Mobile No"
|
||||
"label": "Mobile No",
|
||||
"options": "Mobile"
|
||||
},
|
||||
{
|
||||
"fetch_from": "customer_primary_contact.email_id",
|
||||
"fieldname": "email_id",
|
||||
"fieldtype": "Read Only",
|
||||
"label": "Email Id"
|
||||
"label": "Email Id",
|
||||
"options": "Email"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_26",
|
||||
@@ -570,6 +573,14 @@
|
||||
{
|
||||
"fieldname": "column_break_nwor",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "prospect_name",
|
||||
"fieldtype": "Link",
|
||||
"label": "From Prospect",
|
||||
"no_copy": 1,
|
||||
"options": "Prospect",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-user",
|
||||
@@ -583,7 +594,7 @@
|
||||
"link_fieldname": "party"
|
||||
}
|
||||
],
|
||||
"modified": "2024-05-08 18:03:20.716169",
|
||||
"modified": "2024-06-17 03:24:59.612974",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Customer",
|
||||
@@ -668,4 +679,4 @@
|
||||
"states": [],
|
||||
"title_field": "customer_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ class Customer(TransactionBase):
|
||||
payment_terms: DF.Link | None
|
||||
portal_users: DF.Table[PortalUser]
|
||||
primary_address: DF.Text | None
|
||||
prospect_name: DF.Link | None
|
||||
represents_company: DF.Link | None
|
||||
sales_team: DF.Table[SalesTeam]
|
||||
salutation: DF.Link | None
|
||||
@@ -104,7 +105,7 @@ class Customer(TransactionBase):
|
||||
elif cust_master_name == "Naming Series":
|
||||
set_name_by_naming_series(self)
|
||||
else:
|
||||
self.name = set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self)
|
||||
set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self)
|
||||
|
||||
def get_customer_name(self):
|
||||
if frappe.db.get_value("Customer", self.customer_name) and not frappe.flags.in_import:
|
||||
|
||||
@@ -347,8 +347,8 @@ def make_sales_order(source_name: str, target_doc=None):
|
||||
return _make_sales_order(source_name, target_doc)
|
||||
|
||||
|
||||
def _make_sales_order(source_name, target_doc=None, customer_group=None, ignore_permissions=False):
|
||||
customer = _make_customer(source_name, ignore_permissions, customer_group)
|
||||
def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
|
||||
customer = _make_customer(source_name, ignore_permissions)
|
||||
ordered_items = frappe._dict(
|
||||
frappe.db.get_all(
|
||||
"Sales Order Item",
|
||||
@@ -502,51 +502,71 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
|
||||
return doclist
|
||||
|
||||
|
||||
def _make_customer(source_name, ignore_permissions=False, customer_group=None):
|
||||
def _make_customer(source_name, ignore_permissions=False):
|
||||
quotation = frappe.db.get_value(
|
||||
"Quotation", source_name, ["order_type", "party_name", "customer_name"], as_dict=1
|
||||
"Quotation",
|
||||
source_name,
|
||||
["order_type", "quotation_to", "party_name", "customer_name"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if quotation and quotation.get("party_name"):
|
||||
if not frappe.db.exists("Customer", quotation.get("party_name")):
|
||||
lead_name = quotation.get("party_name")
|
||||
customer_name = frappe.db.get_value(
|
||||
"Customer", {"lead_name": lead_name}, ["name", "customer_name"], as_dict=True
|
||||
)
|
||||
if not customer_name:
|
||||
from erpnext.crm.doctype.lead.lead import _make_customer
|
||||
if quotation.quotation_to == "Customer":
|
||||
return frappe.get_doc("Customer", quotation.party_name)
|
||||
|
||||
customer_doclist = _make_customer(lead_name, ignore_permissions=ignore_permissions)
|
||||
customer = frappe.get_doc(customer_doclist)
|
||||
customer.flags.ignore_permissions = ignore_permissions
|
||||
customer.customer_group = customer_group
|
||||
# Check if a Customer already exists for the Lead or Prospect.
|
||||
existing_customer = None
|
||||
if quotation.quotation_to == "Lead":
|
||||
existing_customer = frappe.db.get_value("Customer", {"lead_name": quotation.party_name})
|
||||
elif quotation.quotation_to == "Prospect":
|
||||
existing_customer = frappe.db.get_value("Customer", {"prospect_name": quotation.party_name})
|
||||
|
||||
try:
|
||||
customer.insert()
|
||||
return customer
|
||||
except frappe.NameError:
|
||||
if frappe.defaults.get_global_default("cust_master_name") == "Customer Name":
|
||||
customer.run_method("autoname")
|
||||
customer.name += "-" + lead_name
|
||||
customer.insert()
|
||||
return customer
|
||||
else:
|
||||
raise
|
||||
except frappe.MandatoryError as e:
|
||||
mandatory_fields = e.args[0].split(":")[1].split(",")
|
||||
mandatory_fields = [customer.meta.get_label(field.strip()) for field in mandatory_fields]
|
||||
if existing_customer:
|
||||
return frappe.get_doc("Customer", existing_customer)
|
||||
|
||||
frappe.local.message_log = []
|
||||
lead_link = frappe.utils.get_link_to_form("Lead", lead_name)
|
||||
message = (
|
||||
_("Could not auto create Customer due to the following missing mandatory field(s):")
|
||||
+ "<br>"
|
||||
)
|
||||
message += "<br><ul><li>" + "</li><li>".join(mandatory_fields) + "</li></ul>"
|
||||
message += _("Please create Customer from Lead {0}.").format(lead_link)
|
||||
# If no Customer exists, create a new Customer or Prospect.
|
||||
if quotation.quotation_to == "Lead":
|
||||
return create_customer_from_lead(quotation.party_name, ignore_permissions=ignore_permissions)
|
||||
elif quotation.quotation_to == "Prospect":
|
||||
return create_customer_from_prospect(quotation.party_name, ignore_permissions=ignore_permissions)
|
||||
|
||||
frappe.throw(message, title=_("Mandatory Missing"))
|
||||
else:
|
||||
return customer_name
|
||||
else:
|
||||
return frappe.get_doc("Customer", quotation.get("party_name"))
|
||||
return None
|
||||
|
||||
|
||||
def create_customer_from_lead(lead_name, ignore_permissions=False):
|
||||
from erpnext.crm.doctype.lead.lead import _make_customer
|
||||
|
||||
customer = _make_customer(lead_name, ignore_permissions=ignore_permissions)
|
||||
customer.flags.ignore_permissions = ignore_permissions
|
||||
|
||||
try:
|
||||
customer.insert()
|
||||
return customer
|
||||
except frappe.MandatoryError as e:
|
||||
handle_mandatory_error(e, customer, lead_name)
|
||||
|
||||
|
||||
def create_customer_from_prospect(prospect_name, ignore_permissions=False):
|
||||
from erpnext.crm.doctype.prospect.prospect import make_customer as make_customer_from_prospect
|
||||
|
||||
customer = make_customer_from_prospect(prospect_name)
|
||||
customer.flags.ignore_permissions = ignore_permissions
|
||||
|
||||
try:
|
||||
customer.insert()
|
||||
return customer
|
||||
except frappe.MandatoryError as e:
|
||||
handle_mandatory_error(e, customer, prospect_name)
|
||||
|
||||
|
||||
def handle_mandatory_error(e, customer, lead_name):
|
||||
from frappe.utils import get_link_to_form
|
||||
|
||||
mandatory_fields = e.args[0].split(":")[1].split(",")
|
||||
mandatory_fields = [customer.meta.get_label(field.strip()) for field in mandatory_fields]
|
||||
|
||||
frappe.local.message_log = []
|
||||
message = _("Could not auto create Customer due to the following missing mandatory field(s):") + "<br>"
|
||||
message += "<br><ul><li>" + "</li><li>".join(mandatory_fields) + "</li></ul>"
|
||||
message += _("Please create Customer from Lead {0}.").format(get_link_to_form("Lead", lead_name))
|
||||
|
||||
frappe.throw(message, title=_("Mandatory Missing"))
|
||||
|
||||
@@ -936,6 +936,10 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None):
|
||||
}
|
||||
|
||||
def set_missing_values(source, target):
|
||||
if kwargs.get("ignore_pricing_rule"):
|
||||
# Skip pricing rule when the dn is creating from the pick list
|
||||
target.ignore_pricing_rule = 1
|
||||
|
||||
target.run_method("set_missing_values")
|
||||
target.run_method("set_po_nos")
|
||||
target.run_method("calculate_taxes_and_totals")
|
||||
|
||||
@@ -3,22 +3,14 @@
|
||||
|
||||
frappe.ui.form.on("Brand", {
|
||||
setup: (frm) => {
|
||||
frm.fields_dict["brand_defaults"].grid.get_field("default_warehouse").get_query = function (
|
||||
doc,
|
||||
cdt,
|
||||
cdn
|
||||
) {
|
||||
frm.set_query("default_warehouse", "brand_defaults", function (doc, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
return {
|
||||
filters: { company: row.company },
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
frm.fields_dict["brand_defaults"].grid.get_field("default_discount_account").get_query = function (
|
||||
doc,
|
||||
cdt,
|
||||
cdn
|
||||
) {
|
||||
frm.set_query("default_discount_account", "brand_defaults", function (doc, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
return {
|
||||
filters: {
|
||||
@@ -27,13 +19,9 @@ frappe.ui.form.on("Brand", {
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
frm.fields_dict["brand_defaults"].grid.get_field("buying_cost_center").get_query = function (
|
||||
doc,
|
||||
cdt,
|
||||
cdn
|
||||
) {
|
||||
frm.set_query("buying_cost_center", "brand_defaults", function (doc, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
return {
|
||||
filters: {
|
||||
@@ -41,25 +29,17 @@ frappe.ui.form.on("Brand", {
|
||||
company: row.company,
|
||||
},
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
frm.fields_dict["brand_defaults"].grid.get_field("expense_account").get_query = function (
|
||||
doc,
|
||||
cdt,
|
||||
cdn
|
||||
) {
|
||||
frm.set_query("expense_account", "brand_defaults", function (doc, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_expense_account",
|
||||
filters: { company: row.company },
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
frm.fields_dict["brand_defaults"].grid.get_field("default_provisional_account").get_query = function (
|
||||
doc,
|
||||
cdt,
|
||||
cdn
|
||||
) {
|
||||
frm.set_query("default_provisional_account", "brand_defaults", function (doc, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
return {
|
||||
filters: {
|
||||
@@ -68,13 +48,9 @@ frappe.ui.form.on("Brand", {
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
frm.fields_dict["brand_defaults"].grid.get_field("selling_cost_center").get_query = function (
|
||||
doc,
|
||||
cdt,
|
||||
cdn
|
||||
) {
|
||||
frm.set_query("selling_cost_center", "brand_defaults", function (doc, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
return {
|
||||
filters: {
|
||||
@@ -82,18 +58,14 @@ frappe.ui.form.on("Brand", {
|
||||
company: row.company,
|
||||
},
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
frm.fields_dict["brand_defaults"].grid.get_field("income_account").get_query = function (
|
||||
doc,
|
||||
cdt,
|
||||
cdn
|
||||
) {
|
||||
frm.set_query("income_account", "brand_defaults", function (doc, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_income_account",
|
||||
filters: { company: row.company },
|
||||
};
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -56,10 +56,11 @@
|
||||
"idx": 1,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2021-03-01 15:57:30.005783",
|
||||
"modified": "2024-08-20 14:10:21.377962",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Brand",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -108,4 +109,4 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ frappe.ui.form.on("Company", {
|
||||
},
|
||||
setup: function (frm) {
|
||||
frm.__rename_queue = "long";
|
||||
erpnext.company.setup_queries(frm);
|
||||
|
||||
frm.set_query("parent_company", function () {
|
||||
return {
|
||||
@@ -81,6 +80,8 @@ frappe.ui.form.on("Company", {
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
erpnext.company.setup_queries(frm);
|
||||
|
||||
frm.toggle_display("address_html", !frm.is_new());
|
||||
|
||||
if (!frm.is_new()) {
|
||||
|
||||
@@ -203,7 +203,7 @@ class Company(NestedSet):
|
||||
):
|
||||
frappe.throw(
|
||||
_("'{0}' should be in company currency {1}.").format(
|
||||
frappe.bold("Default Advance Received Account"), frappe.bold(self.default_currency)
|
||||
frappe.bold(_("Default Advance Received Account")), frappe.bold(self.default_currency)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -214,7 +214,7 @@ class Company(NestedSet):
|
||||
):
|
||||
frappe.throw(
|
||||
_("'{0}' should be in company currency {1}.").format(
|
||||
frappe.bold("Default Advance Paid Account"), frappe.bold(self.default_currency)
|
||||
frappe.bold(_("Default Advance Paid Account")), frappe.bold(self.default_currency)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -447,7 +447,7 @@ class Company(NestedSet):
|
||||
):
|
||||
frappe.throw(
|
||||
_("Set default {0} account for non stock items").format(
|
||||
frappe.bold("Provisional Account")
|
||||
frappe.bold(_("Provisional Account"))
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ class CustomerGroup(NestedSet):
|
||||
|
||||
def validate_currency_for_receivable_and_advance_account(self):
|
||||
for x in self.accounts:
|
||||
company_default_currency = frappe.get_cached_value("Company", x.company, "default_currency")
|
||||
receivable_account_currency = None
|
||||
advance_account_currency = None
|
||||
|
||||
@@ -56,21 +55,6 @@ class CustomerGroup(NestedSet):
|
||||
"Account", x.advance_account, "account_currency"
|
||||
)
|
||||
|
||||
if receivable_account_currency and receivable_account_currency != company_default_currency:
|
||||
frappe.throw(
|
||||
_("Receivable Account: {0} must be in Company default currency: {1}").format(
|
||||
frappe.bold(x.account),
|
||||
frappe.bold(company_default_currency),
|
||||
)
|
||||
)
|
||||
|
||||
if advance_account_currency and advance_account_currency != company_default_currency:
|
||||
frappe.throw(
|
||||
_("Advance Account: {0} must be in Company default currency: {1}").format(
|
||||
frappe.bold(x.advance_account), frappe.bold(company_default_currency)
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
receivable_account_currency
|
||||
and advance_account_currency
|
||||
|
||||
@@ -37,7 +37,6 @@ class SupplierGroup(NestedSet):
|
||||
|
||||
def validate_currency_for_payable_and_advance_account(self):
|
||||
for x in self.accounts:
|
||||
company_default_currency = frappe.get_cached_value("Company", x.company, "default_currency")
|
||||
payable_account_currency = None
|
||||
advance_account_currency = None
|
||||
|
||||
@@ -49,21 +48,6 @@ class SupplierGroup(NestedSet):
|
||||
"Account", x.advance_account, "account_currency"
|
||||
)
|
||||
|
||||
if payable_account_currency and payable_account_currency != company_default_currency:
|
||||
frappe.throw(
|
||||
_("Payable Account: {0} must be in Company default currency: {1}").format(
|
||||
frappe.bold(x.account),
|
||||
frappe.bold(company_default_currency),
|
||||
)
|
||||
)
|
||||
|
||||
if advance_account_currency and advance_account_currency != company_default_currency:
|
||||
frappe.throw(
|
||||
_("Advance Account: {0} must be in Company default currency: {1}").format(
|
||||
frappe.bold(x.advance_account), frappe.bold(company_default_currency)
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
payable_account_currency
|
||||
and advance_account_currency
|
||||
|
||||
@@ -66,29 +66,54 @@ def install(country=None):
|
||||
"parent_item_group": _("All Item Groups"),
|
||||
},
|
||||
# Stock Entry Type
|
||||
{"doctype": "Stock Entry Type", "name": "Material Issue", "purpose": "Material Issue"},
|
||||
{"doctype": "Stock Entry Type", "name": "Material Receipt", "purpose": "Material Receipt"},
|
||||
{
|
||||
"doctype": "Stock Entry Type",
|
||||
"name": "Material Issue",
|
||||
"purpose": "Material Issue",
|
||||
"is_standard": 1,
|
||||
},
|
||||
{
|
||||
"doctype": "Stock Entry Type",
|
||||
"name": "Material Receipt",
|
||||
"purpose": "Material Receipt",
|
||||
"is_standard": 1,
|
||||
},
|
||||
{
|
||||
"doctype": "Stock Entry Type",
|
||||
"name": "Material Transfer",
|
||||
"purpose": "Material Transfer",
|
||||
"is_standard": 1,
|
||||
},
|
||||
{"doctype": "Stock Entry Type", "name": "Manufacture", "purpose": "Manufacture"},
|
||||
{"doctype": "Stock Entry Type", "name": "Repack", "purpose": "Repack"},
|
||||
{
|
||||
"doctype": "Stock Entry Type",
|
||||
"name": "Manufacture",
|
||||
"purpose": "Manufacture",
|
||||
"is_standard": 1,
|
||||
},
|
||||
{
|
||||
"doctype": "Stock Entry Type",
|
||||
"name": "Repack",
|
||||
"purpose": "Repack",
|
||||
"is_standard": 1,
|
||||
},
|
||||
{"doctype": "Stock Entry Type", "name": "Disassemble", "purpose": "Disassemble", "is_standard": 1},
|
||||
{
|
||||
"doctype": "Stock Entry Type",
|
||||
"name": "Send to Subcontractor",
|
||||
"purpose": "Send to Subcontractor",
|
||||
"is_standard": 1,
|
||||
},
|
||||
{
|
||||
"doctype": "Stock Entry Type",
|
||||
"name": "Material Transfer for Manufacture",
|
||||
"purpose": "Material Transfer for Manufacture",
|
||||
"is_standard": 1,
|
||||
},
|
||||
{
|
||||
"doctype": "Stock Entry Type",
|
||||
"name": "Material Consumption for Manufacture",
|
||||
"purpose": "Material Consumption for Manufacture",
|
||||
"is_standard": 1,
|
||||
},
|
||||
# territory: with two default territories, one for home country and one named Rest of the World
|
||||
{
|
||||
|
||||
@@ -188,9 +188,9 @@ class Batch(Document):
|
||||
if has_expiry_date and not self.expiry_date:
|
||||
frappe.throw(
|
||||
msg=_("Please set {0} for Batched Item {1}, which is used to set {2} on Submit.").format(
|
||||
frappe.bold("Shelf Life in Days"),
|
||||
frappe.bold(_("Shelf Life in Days")),
|
||||
get_link_to_form("Item", self.item),
|
||||
frappe.bold("Batch Expiry Date"),
|
||||
frappe.bold(_("Batch Expiry Date")),
|
||||
),
|
||||
title=_("Expiry Date Mandatory"),
|
||||
)
|
||||
|
||||
@@ -267,6 +267,7 @@ class MaterialRequest(BuyingController):
|
||||
mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance")
|
||||
|
||||
for d in self.get("items"):
|
||||
precision = d.precision("ordered_qty")
|
||||
if d.name in mr_items:
|
||||
if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"):
|
||||
d.ordered_qty = flt(mr_items_ordered_qty.get(d.name))
|
||||
@@ -276,14 +277,14 @@ class MaterialRequest(BuyingController):
|
||||
(d.qty + (d.qty * (mr_qty_allowance / 100))), d.precision("ordered_qty")
|
||||
)
|
||||
|
||||
if d.ordered_qty and d.ordered_qty > allowed_qty:
|
||||
if d.ordered_qty and flt(d.ordered_qty, precision) > flt(allowed_qty, precision):
|
||||
frappe.throw(
|
||||
_(
|
||||
"The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than allowed requested quantity {2} for Item {3}"
|
||||
).format(d.ordered_qty, d.parent, allowed_qty, d.item_code)
|
||||
)
|
||||
|
||||
elif d.ordered_qty and d.ordered_qty > d.stock_qty:
|
||||
elif d.ordered_qty and flt(d.ordered_qty, precision) > flt(d.stock_qty, precision):
|
||||
frappe.throw(
|
||||
_(
|
||||
"The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than requested quantity {2} for Item {3}"
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"consider_rejected_warehouses",
|
||||
"get_item_locations",
|
||||
"pick_manually",
|
||||
"ignore_pricing_rule",
|
||||
"section_break_6",
|
||||
"scan_barcode",
|
||||
"column_break_13",
|
||||
@@ -200,11 +201,18 @@
|
||||
"fieldname": "pick_manually",
|
||||
"fieldtype": "Check",
|
||||
"label": "Pick Manually"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If enabled then system won't apply the pricing rule on the delivery note which will be create from the pick list",
|
||||
"fieldname": "ignore_pricing_rule",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Pricing Rule"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 22:49:16.954637",
|
||||
"modified": "2024-08-14 13:20:42.168827",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Pick List",
|
||||
|
||||
@@ -50,6 +50,7 @@ class PickList(Document):
|
||||
customer_name: DF.Data | None
|
||||
for_qty: DF.Float
|
||||
group_same_items: DF.Check
|
||||
ignore_pricing_rule: DF.Check
|
||||
locations: DF.Table[PickListItem]
|
||||
material_request: DF.Link | None
|
||||
naming_series: DF.Literal["STO-PICK-.YYYY.-"]
|
||||
@@ -1144,7 +1145,7 @@ def create_dn_with_so(sales_dict, pick_list):
|
||||
for customer in sales_dict:
|
||||
for so in sales_dict[customer]:
|
||||
delivery_note = None
|
||||
kwargs = {"skip_item_mapping": True}
|
||||
kwargs = {"skip_item_mapping": True, "ignore_pricing_rule": pick_list.ignore_pricing_rule}
|
||||
delivery_note = create_delivery_note_from_sales_order(so, delivery_note, kwargs=kwargs)
|
||||
break
|
||||
if delivery_note:
|
||||
|
||||
@@ -1205,3 +1205,64 @@ class TestPickList(FrappeTestCase):
|
||||
pl_doc.submit()
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "over_picking_allowance", 0)
|
||||
|
||||
def test_ignore_pricing_rule_in_pick_list(self):
|
||||
frappe.flags.print_stmt = False
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item = make_item(
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"batch_number_series": "IPR-PICKLT-.######",
|
||||
"create_new_batch": 1,
|
||||
}
|
||||
).name
|
||||
|
||||
make_stock_entry(
|
||||
item=item,
|
||||
to_warehouse=warehouse,
|
||||
qty=2,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
pricing_rule = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "Same Free Item",
|
||||
"price_or_product_discount": "Product",
|
||||
"selling": 1,
|
||||
"apply_on": "Item Code",
|
||||
"items": [
|
||||
{
|
||||
"item_code": item,
|
||||
}
|
||||
],
|
||||
"same_item": 1,
|
||||
"is_recursive": 1,
|
||||
"recurse_for": 2,
|
||||
"free_qty": 1,
|
||||
"company": "_Test Company",
|
||||
"customer": "_Test Customer",
|
||||
}
|
||||
)
|
||||
|
||||
pricing_rule.save()
|
||||
frappe.flags.print_stmt = True
|
||||
|
||||
so = make_sales_order(item_code=item, qty=2, rate=100, do_not_save=True)
|
||||
so.set_warehouse = warehouse
|
||||
so.submit()
|
||||
|
||||
self.assertEqual(len(so.items), 2)
|
||||
self.assertTrue(so.items[1].is_free_item)
|
||||
|
||||
pl = create_pick_list(so.name)
|
||||
pl.ignore_pricing_rule = 1
|
||||
pl.save()
|
||||
pl.submit()
|
||||
|
||||
self.assertEqual(len(pl.locations), 1)
|
||||
|
||||
delivery_note = create_delivery_note(pl.name)
|
||||
|
||||
self.assertEqual(len(delivery_note.items), 1)
|
||||
|
||||
@@ -837,7 +837,11 @@ class PurchaseReceipt(BuyingController):
|
||||
def update_assets(self, item, valuation_rate):
|
||||
assets = frappe.db.get_all(
|
||||
"Asset",
|
||||
filters={"purchase_receipt": self.name, "item_code": item.item_code},
|
||||
filters={
|
||||
"purchase_receipt": self.name,
|
||||
"item_code": item.item_code,
|
||||
"purchase_receipt_item": ("in", [item.name, ""]),
|
||||
},
|
||||
fields=["name", "asset_quantity"],
|
||||
)
|
||||
|
||||
@@ -1075,6 +1079,7 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
|
||||
if item.billed_amt and item.amount:
|
||||
adjusted_amt = flt(item.billed_amt) - flt(item.amount)
|
||||
|
||||
adjusted_amt = adjusted_amt * flt(pr_doc.conversion_rate)
|
||||
item.db_set("rate_difference_with_purchase_invoice", adjusted_amt, update_modified=False)
|
||||
|
||||
percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, cint, cstr, flt, getdate, nowtime, today
|
||||
from frappe.utils import add_days, cint, cstr, flt, get_datetime, getdate, nowtime, today
|
||||
from pypika import functions as fn
|
||||
|
||||
import erpnext
|
||||
@@ -3592,6 +3592,71 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
inter_transfer_dn.cancel()
|
||||
frappe.db.set_single_value("Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 1)
|
||||
|
||||
def test_sles_with_same_posting_datetime_and_creation(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
from erpnext.stock.report.stock_balance.stock_balance import execute
|
||||
|
||||
item_code = "Test Item for SLE with same posting datetime and creation"
|
||||
create_item(item_code)
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
item_code=item_code,
|
||||
qty=10,
|
||||
rate=100,
|
||||
posting_date="2023-11-06",
|
||||
posting_time="00:00:00",
|
||||
)
|
||||
|
||||
sr = make_stock_entry(
|
||||
item_code=item_code,
|
||||
source=pr.items[0].warehouse,
|
||||
qty=10,
|
||||
posting_date="2023-11-07",
|
||||
posting_time="14:28:0.330404",
|
||||
)
|
||||
|
||||
sle = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": sr.doctype, "voucher_no": sr.name, "item_code": sr.items[0].item_code},
|
||||
"name",
|
||||
)
|
||||
|
||||
sle_doc = frappe.get_doc("Stock Ledger Entry", sle)
|
||||
sle_doc.db_set("creation", "2023-11-07 14:28:01.208930")
|
||||
|
||||
sle_doc.reload()
|
||||
self.assertEqual(get_datetime(sle_doc.creation), get_datetime("2023-11-07 14:28:01.208930"))
|
||||
|
||||
sr = make_stock_entry(
|
||||
item_code=item_code,
|
||||
target=pr.items[0].warehouse,
|
||||
qty=50,
|
||||
posting_date="2023-11-07",
|
||||
posting_time="14:28:0.920825",
|
||||
)
|
||||
|
||||
sle = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": sr.doctype, "voucher_no": sr.name, "item_code": sr.items[0].item_code},
|
||||
"name",
|
||||
)
|
||||
|
||||
sle_doc = frappe.get_doc("Stock Ledger Entry", sle)
|
||||
sle_doc.db_set("creation", "2023-11-07 14:28:01.044561")
|
||||
|
||||
sle_doc.reload()
|
||||
self.assertEqual(get_datetime(sle_doc.creation), get_datetime("2023-11-07 14:28:01.044561"))
|
||||
|
||||
pr.repost_future_sle_and_gle(force=True)
|
||||
|
||||
columns, data = execute(
|
||||
filters=frappe._dict(
|
||||
{"item_code": item_code, "warehouse": pr.items[0].warehouse, "company": pr.company}
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(data[0].get("bal_qty"), 50.0)
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
|
||||
@@ -701,6 +701,57 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
||||
serial_nos = get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||
self.assertEqual(serial_nos, serial_nos1)
|
||||
|
||||
def test_auto_create_serial_and_batch_bundle_for_outward_for_batch_item(self):
|
||||
item_code = make_item(
|
||||
"Test Auto Create Batch Bundle for Outward 1",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"batch_number_series": "ACSBBO-TACSB-.#####",
|
||||
},
|
||||
).name
|
||||
|
||||
if not frappe.db.exists("Batch", "ACSBBO-TACSB-00001"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"batch_id": "ACSBBO-TACSB-00001",
|
||||
"item": item_code,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=10,
|
||||
target="_Test Warehouse - _TC",
|
||||
rate=500,
|
||||
use_serial_batch_fields=True,
|
||||
batch_no="ACSBBO-TACSB-00001",
|
||||
)
|
||||
|
||||
dispatch = make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=10,
|
||||
target="_Test Warehouse - _TC",
|
||||
rate=500,
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
original_value = frappe.db.get_single_value(
|
||||
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward"
|
||||
)
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 0)
|
||||
self.assertRaises(frappe.ValidationError, dispatch.submit)
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 1)
|
||||
dispatch.submit()
|
||||
|
||||
frappe.db.set_single_value(
|
||||
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", original_value
|
||||
)
|
||||
|
||||
|
||||
def get_batch_from_bundle(bundle):
|
||||
from erpnext.stock.serial_batch_bundle import get_batch_nos
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
"label": "Purpose",
|
||||
"oldfieldname": "purpose",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor",
|
||||
"options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor\nDisassemble",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
@@ -143,7 +143,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list([\"Material Transfer for Manufacture\", \"Manufacture\", \"Material Consumption for Manufacture\"], doc.purpose)",
|
||||
"depends_on": "eval:in_list([\"Material Transfer for Manufacture\", \"Manufacture\", \"Material Consumption for Manufacture\", \"Disassemble\"], doc.purpose)",
|
||||
"fieldname": "work_order",
|
||||
"fieldtype": "Link",
|
||||
"label": "Work Order",
|
||||
@@ -242,7 +242,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:in_list([\"Material Issue\", \"Material Transfer\", \"Manufacture\", \"Repack\", \"Send to Subcontractor\", \"Material Transfer for Manufacture\", \"Material Consumption for Manufacture\"], doc.purpose)",
|
||||
"depends_on": "eval:in_list([\"Material Issue\", \"Material Transfer\", \"Manufacture\", \"Repack\", \"Send to Subcontractor\", \"Material Transfer for Manufacture\", \"Material Consumption for Manufacture\", \"Disassemble\"], doc.purpose)",
|
||||
"fieldname": "from_bom",
|
||||
"fieldtype": "Check",
|
||||
"label": "From BOM",
|
||||
@@ -697,7 +697,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-08-13 19:02:42.386955",
|
||||
"modified": "2024-08-13 19:05:42.386955",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Entry",
|
||||
|
||||
@@ -132,6 +132,7 @@ class StockEntry(StockController):
|
||||
"Manufacture",
|
||||
"Repack",
|
||||
"Send to Subcontractor",
|
||||
"Disassemble",
|
||||
]
|
||||
remarks: DF.Text | None
|
||||
sales_invoice_no: DF.Link | None
|
||||
@@ -231,6 +232,7 @@ class StockEntry(StockController):
|
||||
self.validate_serialized_batch()
|
||||
self.calculate_rate_and_amount()
|
||||
self.validate_putaway_capacity()
|
||||
self.validate_component_quantities()
|
||||
|
||||
if not self.get("purpose") == "Manufacture":
|
||||
# ignore scrap item wh difference and empty source/target wh
|
||||
@@ -337,6 +339,7 @@ class StockEntry(StockController):
|
||||
"Repack",
|
||||
"Send to Subcontractor",
|
||||
"Material Consumption for Manufacture",
|
||||
"Disassemble",
|
||||
]
|
||||
|
||||
if self.purpose not in valid_purposes:
|
||||
@@ -616,6 +619,7 @@ class StockEntry(StockController):
|
||||
"Manufacture",
|
||||
"Material Transfer for Manufacture",
|
||||
"Material Consumption for Manufacture",
|
||||
"Disassemble",
|
||||
):
|
||||
# check if work order is entered
|
||||
|
||||
@@ -744,6 +748,34 @@ class StockEntry(StockController):
|
||||
title=_("Insufficient Stock"),
|
||||
)
|
||||
|
||||
def validate_component_quantities(self):
|
||||
if self.purpose not in ["Manufacture", "Material Transfer for Manufacture"]:
|
||||
return
|
||||
|
||||
if not frappe.db.get_single_value("Manufacturing Settings", "validate_components_quantities_per_bom"):
|
||||
return
|
||||
|
||||
if not self.fg_completed_qty:
|
||||
return
|
||||
|
||||
raw_materials = self.get_bom_raw_materials(self.fg_completed_qty)
|
||||
|
||||
precision = frappe.get_precision("Stock Entry Detail", "qty")
|
||||
for row in self.items:
|
||||
if not row.s_warehouse:
|
||||
continue
|
||||
|
||||
if details := raw_materials.get(row.item_code):
|
||||
if flt(details.get("qty"), precision) != flt(row.qty, precision):
|
||||
frappe.throw(
|
||||
_("For the item {0}, the quantity should be {1} according to the BOM {2}.").format(
|
||||
frappe.bold(row.item_code),
|
||||
flt(details.get("qty"), precision),
|
||||
get_link_to_form("BOM", self.bom_no),
|
||||
),
|
||||
title=_("Incorrect Component Quantity"),
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_stock_and_rate(self):
|
||||
"""
|
||||
@@ -898,8 +930,8 @@ class StockEntry(StockController):
|
||||
).format(
|
||||
item.idx,
|
||||
frappe.bold(label),
|
||||
frappe.bold("Manufacture"),
|
||||
frappe.bold("Material Consumption for Manufacture"),
|
||||
frappe.bold(_("Manufacture")),
|
||||
frappe.bold(_("Material Consumption for Manufacture")),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -909,7 +941,7 @@ class StockEntry(StockController):
|
||||
):
|
||||
frappe.throw(
|
||||
_("Only one {0} entry can be created against the Work Order {1}").format(
|
||||
frappe.bold("Manufacture"), frappe.bold(self.work_order)
|
||||
frappe.bold(_("Manufacture")), frappe.bold(self.work_order)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -983,7 +1015,7 @@ class StockEntry(StockController):
|
||||
def set_stock_entry_type(self):
|
||||
if self.purpose:
|
||||
self.stock_entry_type = frappe.get_cached_value(
|
||||
"Stock Entry Type", {"purpose": self.purpose}, "name"
|
||||
"Stock Entry Type", {"purpose": self.purpose, "is_standard": 1}, "name"
|
||||
)
|
||||
|
||||
def set_purpose_for_stock_entry(self):
|
||||
@@ -1703,11 +1735,63 @@ class StockEntry(StockController):
|
||||
},
|
||||
)
|
||||
|
||||
def get_items_for_disassembly(self):
|
||||
"""Get items for Disassembly Order"""
|
||||
|
||||
if not self.work_order:
|
||||
frappe.throw(_("The Work Order is mandatory for Disassembly Order"))
|
||||
|
||||
items = self.get_items_from_manufacture_entry()
|
||||
|
||||
s_warehouse = ""
|
||||
if self.work_order:
|
||||
s_warehouse = frappe.db.get_value("Work Order", self.work_order, "fg_warehouse")
|
||||
|
||||
for row in items:
|
||||
child_row = self.append("items", {})
|
||||
for field, value in row.items():
|
||||
if value is not None:
|
||||
child_row.set(field, value)
|
||||
|
||||
child_row.s_warehouse = (self.from_warehouse or s_warehouse) if row.is_finished_item else ""
|
||||
child_row.t_warehouse = self.to_warehouse if not row.is_finished_item else ""
|
||||
child_row.is_finished_item = 0 if row.is_finished_item else 1
|
||||
|
||||
def get_items_from_manufacture_entry(self):
|
||||
return frappe.get_all(
|
||||
"Stock Entry",
|
||||
fields=[
|
||||
"`tabStock Entry Detail`.`item_code`",
|
||||
"`tabStock Entry Detail`.`item_name`",
|
||||
"`tabStock Entry Detail`.`description`",
|
||||
"`tabStock Entry Detail`.`qty`",
|
||||
"`tabStock Entry Detail`.`transfer_qty`",
|
||||
"`tabStock Entry Detail`.`stock_uom`",
|
||||
"`tabStock Entry Detail`.`uom`",
|
||||
"`tabStock Entry Detail`.`basic_rate`",
|
||||
"`tabStock Entry Detail`.`conversion_factor`",
|
||||
"`tabStock Entry Detail`.`is_finished_item`",
|
||||
"`tabStock Entry Detail`.`batch_no`",
|
||||
"`tabStock Entry Detail`.`serial_no`",
|
||||
"`tabStock Entry Detail`.`use_serial_batch_fields`",
|
||||
],
|
||||
filters=[
|
||||
["Stock Entry", "purpose", "=", "Manufacture"],
|
||||
["Stock Entry", "work_order", "=", self.work_order],
|
||||
["Stock Entry", "docstatus", "=", 1],
|
||||
["Stock Entry Detail", "docstatus", "=", 1],
|
||||
],
|
||||
order_by="`tabStock Entry Detail`.`idx` desc, `tabStock Entry Detail`.`is_finished_item` desc",
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_items(self):
|
||||
self.set("items", [])
|
||||
self.validate_work_order()
|
||||
|
||||
if self.purpose == "Disassemble":
|
||||
return self.get_items_for_disassembly()
|
||||
|
||||
if not self.posting_date or not self.posting_time:
|
||||
frappe.throw(_("Posting date and posting time is mandatory"))
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
from frappe.permissions import add_user_permission, remove_user_permission
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, nowtime, today
|
||||
from frappe.utils import add_days, cstr, flt, get_time, getdate, nowtime, today
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||
from erpnext.stock.doctype.item.test_item import (
|
||||
@@ -1780,6 +1780,74 @@ class TestStockEntry(FrappeTestCase):
|
||||
frappe.db.set_value("Serial and Batch Bundle", sbb, "type_of_transaction", "Inward")
|
||||
self.assertRaises(frappe.ValidationError, se.submit)
|
||||
|
||||
def test_stock_entry_for_same_posting_date_and_time(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item_code = "Test Stock Entry For Same Posting Datetime 1"
|
||||
make_item(item_code, {"is_stock_item": 1})
|
||||
posting_date = nowdate()
|
||||
posting_time = nowtime()
|
||||
|
||||
for index in range(25):
|
||||
se = make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=1,
|
||||
to_warehouse=warehouse,
|
||||
posting_date=posting_date,
|
||||
posting_time=posting_time,
|
||||
do_not_submit=True,
|
||||
purpose="Material Receipt",
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
se.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": item_code,
|
||||
"item_name": se.items[0].item_name,
|
||||
"description": se.items[0].description,
|
||||
"t_warehouse": se.items[0].t_warehouse,
|
||||
"basic_rate": 100,
|
||||
"qty": 1,
|
||||
"stock_qty": 1,
|
||||
"conversion_factor": 1,
|
||||
"expense_account": se.items[0].expense_account,
|
||||
"cost_center": se.items[0].cost_center,
|
||||
"uom": se.items[0].uom,
|
||||
"stock_uom": se.items[0].stock_uom,
|
||||
},
|
||||
)
|
||||
|
||||
se.remarks = f"The current number is {cstr(index)}"
|
||||
|
||||
se.submit()
|
||||
|
||||
sles = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=[
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"actual_qty",
|
||||
"qty_after_transaction",
|
||||
"incoming_rate",
|
||||
"stock_value_difference",
|
||||
"stock_value",
|
||||
],
|
||||
filters={"item_code": item_code, "warehouse": warehouse},
|
||||
order_by="creation",
|
||||
)
|
||||
|
||||
self.assertEqual(len(sles), 50)
|
||||
i = 0
|
||||
for sle in sles:
|
||||
i += 1
|
||||
self.assertEqual(getdate(sle.posting_date), getdate(posting_date))
|
||||
self.assertEqual(get_time(sle.posting_time), get_time(posting_time))
|
||||
self.assertEqual(sle.actual_qty, 1)
|
||||
self.assertEqual(sle.qty_after_transaction, i)
|
||||
self.assertEqual(sle.incoming_rate, 100)
|
||||
self.assertEqual(sle.stock_value_difference, 100)
|
||||
self.assertEqual(sle.stock_value, 100 * i)
|
||||
|
||||
|
||||
def make_serialized_item(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"purpose",
|
||||
"add_to_transit"
|
||||
"add_to_transit",
|
||||
"is_standard"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -16,7 +17,7 @@
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Purpose",
|
||||
"options": "\nMaterial Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor",
|
||||
"options": "\nMaterial Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor\nDisassemble",
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
@@ -26,13 +27,21 @@
|
||||
"fieldname": "add_to_transit",
|
||||
"fieldtype": "Check",
|
||||
"label": "Add to Transit"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_standard",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Standard",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-07-08 08:41:19.385020",
|
||||
"modified": "2024-08-24 16:00:22.696958",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Entry Type",
|
||||
"naming_rule": "Set by user",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
# import frappe
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ class StockEntryType(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
add_to_transit: DF.Check
|
||||
is_standard: DF.Check
|
||||
purpose: DF.Literal[
|
||||
"",
|
||||
"Material Issue",
|
||||
@@ -26,9 +27,25 @@ class StockEntryType(Document):
|
||||
"Manufacture",
|
||||
"Repack",
|
||||
"Send to Subcontractor",
|
||||
"Disassemble",
|
||||
]
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_standard_type()
|
||||
if self.add_to_transit and self.purpose != "Material Transfer":
|
||||
self.add_to_transit = 0
|
||||
|
||||
def validate_standard_type(self):
|
||||
if self.is_standard and self.name not in [
|
||||
"Material Issue",
|
||||
"Material Receipt",
|
||||
"Material Transfer",
|
||||
"Material Transfer for Manufacture",
|
||||
"Material Consumption for Manufacture",
|
||||
"Manufacture",
|
||||
"Repack",
|
||||
"Send to Subcontractor",
|
||||
"Disassemble",
|
||||
]:
|
||||
frappe.throw(f"Stock Entry Type {self.name} cannot be set as standard")
|
||||
|
||||
@@ -3,6 +3,33 @@
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
class TestStockEntryType(unittest.TestCase):
|
||||
pass
|
||||
def test_stock_entry_type_non_standard(self):
|
||||
stock_entry_type = "Test Manufacturing"
|
||||
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Stock Entry Type",
|
||||
"__newname": stock_entry_type,
|
||||
"purpose": "Manufacture",
|
||||
"is_standard": 1,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, doc.insert)
|
||||
|
||||
def test_stock_entry_type_is_standard(self):
|
||||
for stock_entry_type in [
|
||||
"Material Issue",
|
||||
"Material Receipt",
|
||||
"Material Transfer",
|
||||
"Material Transfer for Manufacture",
|
||||
"Material Consumption for Manufacture",
|
||||
"Manufacture",
|
||||
"Repack",
|
||||
"Send to Subcontractor",
|
||||
]:
|
||||
self.assertTrue(frappe.db.get_value("Stock Entry Type", stock_entry_type, "is_standard"))
|
||||
|
||||
@@ -352,7 +352,8 @@
|
||||
{
|
||||
"fieldname": "posting_datetime",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Posting Datetime"
|
||||
"label": "Posting Datetime",
|
||||
"search_index": 1
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
@@ -361,7 +362,7 @@
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-27 16:23:18.820049",
|
||||
"modified": "2024-08-27 09:28:03.961443",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Ledger Entry",
|
||||
|
||||
@@ -1043,6 +1043,8 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
self.assertEqual(50, _get_stock_credit(final_consumption))
|
||||
|
||||
def test_tie_breaking(self):
|
||||
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import repost_entries
|
||||
|
||||
frappe.flags.dont_execute_stock_reposts = True
|
||||
self.addCleanup(frappe.flags.pop, "dont_execute_stock_reposts")
|
||||
|
||||
@@ -1085,6 +1087,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
self.assertEqual([10, 11], ordered_qty_after_transaction())
|
||||
|
||||
first.cancel()
|
||||
repost_entries()
|
||||
self.assertEqual([1], ordered_qty_after_transaction())
|
||||
|
||||
backdated = make_stock_entry(
|
||||
@@ -1184,7 +1187,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
qty=5,
|
||||
posting_date="2021-01-01",
|
||||
rate=10,
|
||||
posting_time="02:00:00.1234",
|
||||
posting_time="02:00:00",
|
||||
)
|
||||
|
||||
time.sleep(3)
|
||||
@@ -1196,7 +1199,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
qty=100,
|
||||
rate=10,
|
||||
posting_date="2021-01-01",
|
||||
posting_time="02:00:00",
|
||||
posting_time="02:00:00.1234",
|
||||
)
|
||||
|
||||
sle = frappe.get_all(
|
||||
|
||||
@@ -255,7 +255,7 @@ class StockReservationEntry(Document):
|
||||
if self.has_batch_no
|
||||
else _("Warehouse"),
|
||||
frappe.bold(self.warehouse),
|
||||
frappe.bold("Stock Reservation Entry"),
|
||||
frappe.bold(_("Stock Reservation Entry")),
|
||||
)
|
||||
|
||||
frappe.throw(msg)
|
||||
@@ -497,7 +497,8 @@ def validate_stock_reservation_settings(voucher: object) -> None:
|
||||
|
||||
if not frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"):
|
||||
msg = _("Please enable {0} in the {1}.").format(
|
||||
frappe.bold("Stock Reservation"), frappe.bold("Stock Settings")
|
||||
frappe.bold(_("Stock Reservation")),
|
||||
frappe.bold(_("Stock Settings")),
|
||||
)
|
||||
frappe.throw(msg)
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ class StockSettings(Document):
|
||||
if self.allow_negative_stock and self.enable_stock_reservation:
|
||||
frappe.throw(
|
||||
_("As {0} is enabled, you can not enable {1}.").format(
|
||||
frappe.bold("Stock Reservation"), frappe.bold("Allow Negative Stock")
|
||||
frappe.bold(_("Stock Reservation")), frappe.bold(_("Allow Negative Stock"))
|
||||
)
|
||||
)
|
||||
|
||||
@@ -187,7 +187,7 @@ class StockSettings(Document):
|
||||
if self.allow_negative_stock:
|
||||
frappe.throw(
|
||||
_("As {0} is enabled, you can not enable {1}.").format(
|
||||
frappe.bold("Allow Negative Stock"), frappe.bold("Stock Reservation")
|
||||
frappe.bold(_("Allow Negative Stock")), frappe.bold(_("Stock Reservation"))
|
||||
)
|
||||
)
|
||||
|
||||
@@ -207,7 +207,7 @@ class StockSettings(Document):
|
||||
if bin_with_negative_stock:
|
||||
frappe.throw(
|
||||
_("As there are negative stock, you can not enable {0}.").format(
|
||||
frappe.bold("Stock Reservation")
|
||||
frappe.bold(_("Stock Reservation"))
|
||||
)
|
||||
)
|
||||
|
||||
@@ -221,7 +221,7 @@ class StockSettings(Document):
|
||||
if has_reserved_stock:
|
||||
frappe.throw(
|
||||
_("As there are reserved stock, you cannot disable {0}.").format(
|
||||
frappe.bold("Stock Reservation")
|
||||
frappe.bold(_("Stock Reservation"))
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -263,7 +263,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2024-01-24 16:27:28.299520",
|
||||
"modified": "2024-08-14 16:08:15.733597",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Warehouse",
|
||||
@@ -320,6 +320,5 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "warehouse_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -807,14 +807,10 @@ def get_price_list_rate(args, item_doc, out=None):
|
||||
if price_list_rate is None or frappe.db.get_single_value(
|
||||
"Stock Settings", "update_existing_price_list_rate"
|
||||
):
|
||||
if args.get("is_internal_supplier") or args.get("is_internal_customer"):
|
||||
return out
|
||||
insert_item_price(args)
|
||||
|
||||
if args.price_list and args.rate:
|
||||
insert_item_price(args)
|
||||
|
||||
if not price_list_rate:
|
||||
return out
|
||||
if price_list_rate is None:
|
||||
return out
|
||||
|
||||
out.price_list_rate = flt(price_list_rate) * flt(args.plc_conversion_rate) / flt(args.conversion_rate)
|
||||
|
||||
@@ -835,6 +831,14 @@ def get_price_list_rate(args, item_doc, out=None):
|
||||
|
||||
def insert_item_price(args):
|
||||
"""Insert Item Price if Price List and Price List Rate are specified and currency is the same"""
|
||||
if (
|
||||
not args.price_list
|
||||
or not args.rate
|
||||
or args.get("is_internal_supplier")
|
||||
or args.get("is_internal_customer")
|
||||
):
|
||||
return
|
||||
|
||||
if frappe.db.get_value("Price List", args.price_list, "currency", cache=True) == args.currency and cint(
|
||||
frappe.db.get_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing")
|
||||
):
|
||||
|
||||
@@ -354,9 +354,14 @@ def get_email_list(company):
|
||||
|
||||
|
||||
def get_comapny_wise_users(company):
|
||||
companies = [company]
|
||||
|
||||
if parent_company := frappe.db.get_value("Company", company, "parent_company"):
|
||||
companies.append(parent_company)
|
||||
|
||||
users = frappe.get_all(
|
||||
"User Permission",
|
||||
filters={"allow": "Company", "for_value": company, "apply_to_all_doctypes": 1},
|
||||
filters={"allow": "Company", "for_value": ("in", companies), "apply_to_all_doctypes": 1},
|
||||
fields=["user"],
|
||||
)
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ def get_stock_ledger_entries_for_batch_bundle(filters):
|
||||
& (sle.has_batch_no == 1)
|
||||
& (sle.posting_date <= filters["to_date"])
|
||||
)
|
||||
.groupby(batch_package.batch_no, batch_package.warehouse)
|
||||
.groupby(sle.voucher_no, batch_package.batch_no, batch_package.warehouse)
|
||||
.orderby(sle.item_code, sle.warehouse)
|
||||
)
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ class SerialBatchBundle:
|
||||
}
|
||||
|
||||
if self.sle.actual_qty < 0 and self.is_material_transfer():
|
||||
values_to_update["valuation_rate"] = sn_doc.avg_rate
|
||||
values_to_update["valuation_rate"] = flt(sn_doc.avg_rate)
|
||||
|
||||
if not frappe.db.get_single_value(
|
||||
"Stock Settings", "do_not_update_serial_batch_on_creation_of_auto_bundle"
|
||||
@@ -221,7 +221,15 @@ class SerialBatchBundle:
|
||||
not self.sle.is_cancelled
|
||||
and not self.sle.serial_and_batch_bundle
|
||||
and self.item_details.has_batch_no == 1
|
||||
and self.item_details.create_new_batch
|
||||
and (
|
||||
self.item_details.create_new_batch
|
||||
or (
|
||||
frappe.db.get_single_value(
|
||||
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward"
|
||||
)
|
||||
and self.sle.actual_qty < 0
|
||||
)
|
||||
)
|
||||
):
|
||||
self.make_serial_batch_no_bundle()
|
||||
elif not self.sle.is_cancelled:
|
||||
|
||||
@@ -647,6 +647,7 @@ class update_entries_after:
|
||||
and (
|
||||
posting_datetime = %(posting_datetime)s
|
||||
)
|
||||
and creation = %(creation)s
|
||||
order by
|
||||
creation ASC
|
||||
for update
|
||||
@@ -1526,7 +1527,12 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc
|
||||
voucher_no = args.get("voucher_no")
|
||||
voucher_condition = f"and voucher_no != '{voucher_no}'"
|
||||
|
||||
sle = frappe.db.sql(
|
||||
elif args.get("creation"):
|
||||
creation = args.get("creation")
|
||||
operator = "<="
|
||||
voucher_condition = f"and creation < '{creation}'"
|
||||
|
||||
sle = frappe.db.sql( # nosemgrep
|
||||
f"""
|
||||
select *, posting_datetime as "timestamp"
|
||||
from `tabStock Ledger Entry`
|
||||
@@ -1537,7 +1543,7 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc
|
||||
and (
|
||||
posting_datetime {operator} %(posting_datetime)s
|
||||
)
|
||||
order by posting_datetime desc, creation desc
|
||||
order by posting_date desc, posting_time desc, creation desc
|
||||
limit 1
|
||||
for update""",
|
||||
{
|
||||
@@ -1623,6 +1629,7 @@ def get_stock_ledger_entries(
|
||||
if extra_cond:
|
||||
conditions += f"{extra_cond}"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select *, posting_datetime as "timestamp"
|
||||
@@ -1630,7 +1637,7 @@ def get_stock_ledger_entries(
|
||||
where item_code = %(item_code)s
|
||||
and is_cancelled = 0
|
||||
{conditions}
|
||||
order by posting_datetime {order}, creation {order}
|
||||
order by posting_date {order}, posting_time {order}, creation {order}
|
||||
{limit} {for_update}""".format(
|
||||
conditions=conditions,
|
||||
limit=limit or "",
|
||||
@@ -1738,7 +1745,7 @@ def get_valuation_rate(
|
||||
return batch_obj.get_incoming_rate()
|
||||
|
||||
# Get valuation rate from last sle for the same item and warehouse
|
||||
if last_valuation_rate := frappe.db.sql(
|
||||
if last_valuation_rate := frappe.db.sql( # nosemgrep
|
||||
"""select valuation_rate
|
||||
from `tabStock Ledger Entry` force index (item_warehouse)
|
||||
where
|
||||
@@ -1747,7 +1754,7 @@ def get_valuation_rate(
|
||||
AND valuation_rate >= 0
|
||||
AND is_cancelled = 0
|
||||
AND NOT (voucher_no = %s AND voucher_type = %s)
|
||||
order by posting_datetime desc, name desc limit 1""",
|
||||
order by posting_date desc, posting_time desc, name desc limit 1""",
|
||||
(item_code, warehouse, voucher_no, voucher_type),
|
||||
):
|
||||
return flt(last_valuation_rate[0][0])
|
||||
@@ -1818,7 +1825,7 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
|
||||
detail = next_stock_reco_detail[0]
|
||||
datetime_limit_condition = get_datetime_limit_condition(detail)
|
||||
|
||||
frappe.db.sql(
|
||||
frappe.db.sql( # nosemgrep
|
||||
f"""
|
||||
update `tabStock Ledger Entry`
|
||||
set qty_after_transaction = qty_after_transaction + {qty_shift}
|
||||
@@ -1984,8 +1991,8 @@ def is_negative_with_precision(neg_sle, is_batch=False):
|
||||
return qty_deficit < 0 and abs(qty_deficit) > 0.0001
|
||||
|
||||
|
||||
def get_future_sle_with_negative_qty(args):
|
||||
return frappe.db.sql(
|
||||
def get_future_sle_with_negative_qty(sle_args):
|
||||
return frappe.db.sql( # nosemgrep
|
||||
"""
|
||||
select
|
||||
qty_after_transaction, posting_date, posting_time,
|
||||
@@ -1998,28 +2005,28 @@ def get_future_sle_with_negative_qty(args):
|
||||
and posting_datetime >= %(posting_datetime)s
|
||||
and is_cancelled = 0
|
||||
and qty_after_transaction < 0
|
||||
order by posting_datetime asc
|
||||
order by posting_date asc, posting_time asc
|
||||
limit 1
|
||||
""",
|
||||
args,
|
||||
sle_args,
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
def get_future_sle_with_negative_batch_qty(args):
|
||||
return frappe.db.sql(
|
||||
def get_future_sle_with_negative_batch_qty(sle_args):
|
||||
return frappe.db.sql( # nosemgrep
|
||||
"""
|
||||
with batch_ledger as (
|
||||
select
|
||||
posting_date, posting_time, posting_datetime, voucher_type, voucher_no,
|
||||
sum(actual_qty) over (order by posting_datetime, creation) as cumulative_total
|
||||
sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
|
||||
from `tabStock Ledger Entry`
|
||||
where
|
||||
item_code = %(item_code)s
|
||||
and warehouse = %(warehouse)s
|
||||
and batch_no=%(batch_no)s
|
||||
and is_cancelled = 0
|
||||
order by posting_datetime, creation
|
||||
order by posting_date, posting_time, creation
|
||||
)
|
||||
select * from batch_ledger
|
||||
where
|
||||
@@ -2027,7 +2034,7 @@ def get_future_sle_with_negative_batch_qty(args):
|
||||
and posting_datetime >= %(posting_datetime)s
|
||||
limit 1
|
||||
""",
|
||||
args,
|
||||
sle_args,
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ class ServiceLevelAgreement(Document):
|
||||
):
|
||||
frappe.throw(
|
||||
_("{0} is not enabled in {1}").format(
|
||||
frappe.bold("Track Service Level Agreement"),
|
||||
frappe.bold(_("Track Service Level Agreement")),
|
||||
get_link_to_form("Support Settings", "Support Settings"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1095,7 +1095,7 @@ Guardian1 Name,Guardian1 Namen,
|
||||
Guardian2 Email ID,Guardian2 E-Mail-ID,
|
||||
Guardian2 Mobile No,Guardian2 Mobil Nein,
|
||||
Guardian2 Name,Guardian2 Namen,
|
||||
HR Manager,Leiter der Personalabteilung,
|
||||
HR Manager,Personalwesen Leiter,
|
||||
HSN,HSN,
|
||||
HSN/SAC,HSN / SAC,
|
||||
Half Yearly,Halbjährlich,
|
||||
@@ -3728,6 +3728,7 @@ Subdivision,Teilgebiet,
|
||||
Submit Review,Bewertung abschicken,
|
||||
Submitted,Gebucht,
|
||||
Supplier Addresses And Contacts,Lieferanten-Adressen und Kontaktdaten,
|
||||
Supplier Address Details,Vorschau Lieferantenadresse,
|
||||
Synchronize this account,Synchronisieren Sie dieses Konto,
|
||||
Tag,Etikett,
|
||||
Target Location is required while receiving Asset {0} from an employee,"Der Zielspeicherort ist erforderlich, wenn Asset {0} von einem Mitarbeiter empfangen wird",
|
||||
@@ -6027,10 +6028,10 @@ Hotel Room Reservation Item,Hotelzimmer-Reservierungselement,
|
||||
Hotel Settings,Hoteleinstellungen,
|
||||
Default Taxes and Charges,Standard-Steuern und -Abgaben,
|
||||
Default Invoice Naming Series,Standard-Rechnungsnummernkreis,
|
||||
HR,HR,
|
||||
HR,Personalwesen,
|
||||
Date on which this component is applied,"Datum, an dem diese Komponente angewendet wird",
|
||||
Salary Slip,Gehaltsabrechnung,
|
||||
HR User,Nutzer Personalabteilung,
|
||||
HR User,Personalwesen Benutzer,
|
||||
Job Applicant,Bewerber,
|
||||
Body,Körper,
|
||||
Appraisal Template,Bewertungsvorlage,
|
||||
@@ -9917,8 +9918,8 @@ Bank/Cash Account {0} doesn't belong to company {1},Das Bank- / Kassenkonto {0}
|
||||
Base Amount,Basisbetrag,
|
||||
Base Total Billed Amount,Insg. abgerechneter Betrag in Basiswährung,
|
||||
Base Total Costing Amount,Gesamtkosten in Basiswährung,
|
||||
"Based on your HR Policy, select your leave allocation period's end date",Wählen Sie auf der Grundlage Ihrer HR-Richtlinien das Enddatum Ihres Abwesenheitskontingents aus,
|
||||
"Based on your HR Policy, select your leave allocation period's start date",Wählen Sie auf der Grundlage Ihrer HR-Richtlinie das Startdatum Ihres Abwesenheitskontingents aus,
|
||||
"Based on your HR Policy, select your leave allocation period's end date",Wählen Sie auf der Grundlage Ihrer Personal-Richtlinien das Enddatum Ihres Abwesenheitskontingents aus,
|
||||
"Based on your HR Policy, select your leave allocation period's start date",Wählen Sie auf der Grundlage Ihrer Personal-Richtlinie das Startdatum Ihres Abwesenheitskontingents aus,
|
||||
Batch No is mandatory,Chargennummer ist obligatorisch,
|
||||
Batch No {0} does not exists,Charge Nr. {0} existiert nicht,
|
||||
Batch No {0} is linked with Item {1} which has serial no. Please scan serial no instead.,"Die Chargennummer {0} ist mit dem Artikel {1} verknüpft, der eine Seriennummer hat. Bitte scannen Sie stattdessen die Seriennummer.",
|
||||
@@ -11472,7 +11473,7 @@ Shipment Parcel,Versandpaket,
|
||||
Shipment Parcel Template,Versandpaketvorlage,
|
||||
Shipment Type,Sendungstyp,
|
||||
Shipment details,Sendungsdetails,
|
||||
Shipping Address Details,Lieferadressendetails,
|
||||
Shipping Address Details,Vorschau Lieferadresse,
|
||||
Shipping Address Template,Vorlage Lieferadresse,
|
||||
Show Balances in Chart Of Accounts,Saldo in Kontenplan anzeigen,
|
||||
Show Barcode Field in Stock Transactions,Barcode-Feld in Lagerbewegungen anzeigen,
|
||||
@@ -11815,3 +11816,103 @@ will be,wird sein,
|
||||
{} is a child company.,{} ist ein untergeordnetes Unternehmen.,
|
||||
{} {} is already linked with another {},{} {} ist bereits mit einem anderen {} verknüpft,
|
||||
{} {} is already linked with {} {},{} {} ist bereits mit {} {} verknüpft,
|
||||
A Transaction Deletion Document: {0} is triggered for {0},Eine Transaktion Löschungsdokument: {0} wird für {0} ausgelöst,
|
||||
About Us Settings,"Einstellungen zu ""Über uns""",
|
||||
Allow Internal Transfers at Arm's Length Price,Interne Übertragungen zum Fremdvergleichspreis zulassen,
|
||||
Asset decapitalized after Asset Capitalization {0} was submitted,"Vermögenswert freigegeben, nachdem Anlagenaktivierung {0} gebucht wurde",
|
||||
Auto Email Report,Auto Email-Bericht,
|
||||
Auto close Opportunity Replied after the no. of days mentioned above,Automatische Schließungschaltung antwortete nach der oben genannten Anzahl von Tagen,
|
||||
Avg Rate (Balance Stock),Durchschnittliche Rate (Lagerbestand),
|
||||
Billing Interval in Subscription Plan must be Month to follow calendar months,"Abrechnungsintervall im Abonnementplan muss ""Monat"" sein, um Kalendermonate zu folgen",
|
||||
Bulk Update,Massen-Update,
|
||||
Can't disable batch wise valuation for active batches.,Sie können die chargenweise Bewertung für aktive Chargen nicht deaktivieren.,
|
||||
Can't disable batch wise valuation for items with FIFO valuation method.,Sie können die chargenweise Bewertung für Artikel mit FIFO-Bewertungsmethode nicht deaktivieren.,
|
||||
Cannot disable batch wise valuation for FIFO valuation method.,Sie können die chargenweise Bewertung für die FIFO-Bewertungsmethode nicht deaktivieren.,
|
||||
Cannot enqueue multi docs for one company. {0} is already queued/running for company: {1},Mehrere Dokumente für ein Unternehmen können nicht in die Warteschlange gestellt werden. {0} ist bereits in die Warteschlange gestellt/wird für das Unternehmen ausgeführt: {1},
|
||||
Contact Us Settings,Einstellungen zu „Kontaktieren Sie uns“,
|
||||
Create Journal Entries,Buchungssätze erstellen,
|
||||
Create a variant with the template image.,Eine Variante mit dem Vorlagenbild erstellen.,
|
||||
Create in Draft Status,In Entwurfsstatus erstellen,
|
||||
Custom delimiters,Benutzerdefinierte Trennzeichen,
|
||||
Deleted Documents,Gelöschte Dokumente,
|
||||
Delimiter options,Trennzeichenoptionen,
|
||||
Dependent Task {0} is not a Template Task,Abhängige Aufgabe {0} ist keine Vorlage einer Aufgabe,
|
||||
Depreciation Entry Posting Status,Buchungsstatus des Abschreibungseintrags,
|
||||
Depreciation Schedule View,Ansicht Abschreibungsplan,
|
||||
Depreciation cannot be calculated for fully depreciated assets,Für vollständig abgeschriebene Vermögensgegenstände kann keine Abschreibung berechnet werden,
|
||||
Do Not Use Batch-wise Valuation,Keine chargenweise Bewertung verwenden,
|
||||
Domain Settings,Domäneneinstellungen,
|
||||
Email Domain,E-Mail-Domain,
|
||||
Enable Immutable Ledger,Unveränderliches Hauptbuch aktivieren,
|
||||
Enable it if users want to consider rejected materials to dispatch.,"Aktivieren Sie diese Option, wenn Benutzer zurückgewiesenes Material für den Versand berücksichtigen möchten.",
|
||||
Excess Materials Consumed,Überschüssige Materialien verbraucht,
|
||||
Excess Transfer,Überschuss-Übertragung,
|
||||
FIFO Queue vs Qty After Transaction Comparison,Vergleich zwischen FIFO-Warteschlange und Menge nach Transaktion,
|
||||
"For the {0}, the quantity is required to make the return entry","Für die {0} ist die Menge erforderlich, um die Retoure zu erstellen",
|
||||
"If enabled, the item rate won't adjust to the valuation rate during internal transfers, but accounting will still use the valuation rate.","Falls aktiviert, wird der Artikelkurs bei internen Transfers nicht an den Bewertungskurs angepasst, aber die Buchhaltung verwendet weiterhin den Wertansatz.",
|
||||
"If enabled, the system will use the moving average valuation method to calculate the valuation rate for the batched items and will not consider the individual batch-wise incoming rate.","Falls aktiviert, verwendet das System die Bewertungsmethode des gleitenden Durchschnitts zur Berechnung des Wertansatzes für die chargenweisen Artikel und berücksichtigt nicht den individuellen chargenweisen Eingangskurs.",
|
||||
Job Worker,Unterauftragnehmer,
|
||||
Job Worker Address,Unterauftragnehmer Adresse,
|
||||
Job Worker Address Details,Vorschau Adresse Unterauftragnehmer,
|
||||
Job Worker Contact,Vertrag des Unterauftragnehmers,
|
||||
Job Worker Delivery Note,Lieferschein des Unterauftragnehmers,
|
||||
Job Worker Name,Name des Unterauftragnehmer,
|
||||
Job Worker Warehouse,Lagerhaus des Unterauftragnehmers,
|
||||
"Learn about <a href=""https://docs.erpnext.com/docs/v13/user/manual/en/accounts/articles/common_party_accounting#:~:text=Common%20Party%20Accounting%20in%20ERPNext,Invoice%20against%20a%20primary%20Supplier."">Common Party</a>","Erfahren Sie mehr über die <a href=""https://docs.erpnext.com/docs/v13/user/manual/en/accounts/articles/common_party_accounting#:~:text=Common%20Party%20Accounting%20in%20ERPNext,Invoice%20against%20a%20primary%20Supplier."">Verknüpfung von Kunden und Lieferanten</a>",
|
||||
Notification,Benachrichtigung,
|
||||
Notification Settings,Benachrichtigungseinstellungen,
|
||||
Offsetting for Accounting Dimension,Verrechnung für Buchhaltungsdimension,
|
||||
Only Include Allocated Payments,Nur zugeordnete Zahlungen einbeziehen,
|
||||
Only one {0} entry can be created against the Work Order {1},Nur ein {0} Eintrag kann gegen den Arbeitsauftrag {1} erstellt werden,
|
||||
Over Picking Allowance,Überkommissionierzugabe,
|
||||
Over Transfer Allowance,Überschlusstransferzugabe,
|
||||
Overbilling of {} ignored because you have {} role.,"Überhöhte Abrechnung von {} wurde ignoriert, weil Sie die Rolle {} haben.",
|
||||
Payment Reconciliation Job: {0} is running for this party. Can't reconcile now.,Job für Zahlungsabgleich: {0} läuft für diese Partei. Kann jetzt nicht abgleichen.,
|
||||
Payment Request created from Sales Order or Purchase Order will be in Draft status. When disabled document will be in unsaved state.,"Eine Zahlungsanforderung, die aus einem Auftrag oder einer Bestellung erstellt wurde, wird im Entwurfsstatus sein. Wenn deaktiviert, wird das Dokument in ungespeichertem Zustand sein.",
|
||||
Payment Request took too long to respond. Please try requesting for payment again.,"Zahlungsaufforderung hat zu lange gedauert, um zu antworten. Bitte versuchen Sie die Zahlung erneut anzufragen.",
|
||||
Payment Terms Status for Sales Order,Status für Zahlungsbedingungen für Aufträge,
|
||||
Pipeline By,Pipeline von,
|
||||
Please enable Use Old Serial / Batch Fields to make_bundle,"Bitte aktivieren Sie ""Alte Serien-/Batchfelder verwenden"" für make_bundle",
|
||||
Print Style,Druckstil,
|
||||
Reconcile All Serial Nos / Batches,Alle Seriennummern/Chargen abgleichen,
|
||||
Reset Company Default Values,Standardwerte des Unternehmens zurücksetzen,
|
||||
Reset Raw Materials Table,Tabelle Rohstoffe zurücksetzen,
|
||||
Return Against Subcontracting Receipt,Retoure gegen Unterauftragsbeleg,
|
||||
Return Components,Komponenten zurückgeben,
|
||||
Returned Against,Zurückgegeben gegen,
|
||||
Returned exchange rate is neither integer not float.,Der zurückgegebene Wechselkurs ist weder Integer noch Float.,
|
||||
Round Off Tax Amount,Steuerbetrag abrunden,
|
||||
Rounding Loss Allowance,Rundungsverlusttoleranz,
|
||||
Rounding Loss Allowance should be between 0 and 1,Rundungsverlusttoleranz muss zwischen 0 und 1 sein,
|
||||
Row #{0}: Rejected Warehouse is mandatory for the rejected Item {1},Zeile #{0}: Ausschusslager ist für den abgelehnten Artikel {1} obligatorisch,
|
||||
Row #{0}: You cannot use the inventory dimension '{1}' in Stock Reconciliation to modify the quantity or valuation rate. Stock reconciliation with inventory dimensions is intended solely for performing opening entries.,"Zeile #{0}: Sie können die Bestandsdimension '{1}' in der Bestandsabgleich nicht verwenden, um die Menge oder den Wertansatz zu ändern. Die Bestandsabgleich mit Bestandsdimensionen ist ausschließlich für die Durchführung von Eröffnungsbuchungen vorgesehen.",
|
||||
Row {0}: Packed Qty must be equal to {1} Qty.,Zeile {0}: Verpackte Menge muss gleich der {1} Menge sein.,
|
||||
Row {0}: Total Number of Depreciations cannot be less than or equal to Opening Number of Booked Depreciations,Zeile {0}: Die Gesamtzahl der Abschreibungen kann nicht kleiner oder gleich der Anzahl der gebuchten Abschreibungen zu Beginn sein,
|
||||
SCO Supplied Item,Artikel beigestellt für Unterauftrag,
|
||||
SLA Fulfilled On Status,SLA erfüllt am Status,
|
||||
SLA will be applied if {1} is set as {2}{3},"SLA wird angewendet, wenn {1} als {2}{3} eingestellt ist",
|
||||
SMS Settings,SMS-Einstellungen,
|
||||
SO Total Qty,Kd.-Auftr.-Gesamtmenge,
|
||||
"Sales Order {0} already exists against Customer's Purchase Order {1}. To allow multiple Sales Orders, Enable {2} in {3}","Auftrag {0} existiert bereits für die Kundenbestellung {1}. Um mehrere Verkaufsaufträge zuzulassen, aktivieren Sie {2} in {3}",
|
||||
"Scorecard variables can be used, as well as:
|
||||
{total_score} (the total score from that period),
|
||||
{period_number} (the number of periods to present day)
|
||||
","Variablen der Bewertung können verwendet werden, sowie:
|
||||
{total_score} (die Gesamtpunktzahl aus diesem Zeitraum),
|
||||
{period_number} (die Anzahl der Zeiträume bis zum heutigen Tag)
|
||||
",
|
||||
Select Accounting Dimension.,Buchhaltungsdimension auswählen,
|
||||
Select Corrective Operation,Nacharbeit auswählen,
|
||||
Select Date of Birth. This will validate Employees age and prevent hiring of under-age staff.,Wählen Sie Geburtsdatum. Damit wird das Alter der Mitarbeiter überprüft und die Einstellung von minderjährigen Mitarbeitern verhindert.,
|
||||
"Select Date of joining. It will have impact on the first salary calculation, Leave allocation on pro-rata bases.",Wählen Sie Eintrittsdatum. Es wirkt sich auf die erste Gehaltsberechnung und die Zuteilung von Abwesenheiten auf Pro-rata-Basis aus.,
|
||||
Select Dimension,Dimension auswählen,
|
||||
Select Items for Quality Inspection,Artikel für die Qualitätsprüfung auswählen,
|
||||
Select Job Worker Address,Unterauftragnehmer Adresse auswählen,
|
||||
Service Expenses,Wartungsaufwand,
|
||||
Service Level Agreement for {0} {1} already exists.,Service Level Agreement für {0} {1} existiert bereits.,
|
||||
System Settings,Systemverwaltung,
|
||||
Website Script,Webseiten-Skript,
|
||||
Website Theme,Webseiten-Thema,
|
||||
Workflow Action,Workflow-Aktion,
|
||||
Workflow State,Workflow-Status,
|
||||
{0} is not running. Cannot trigger events for this Document,{0} läuft nicht. Ereignisse für dieses Dokument können nicht ausgelöst werden,
|
||||
|
||||
|
Can't render this file because it is too large.
|
Reference in New Issue
Block a user