mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-23 12:50:40 +00:00
Compare commits
167 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f376009d0 | ||
|
|
2ac0009b87 | ||
|
|
307e394da4 | ||
|
|
a293ec0db3 | ||
|
|
d4aae4f311 | ||
|
|
4dd58aba78 | ||
|
|
0b20a5d82e | ||
|
|
5039f45be4 | ||
|
|
a755540708 | ||
|
|
81afdcf745 | ||
|
|
f7e165b5ff | ||
|
|
9a58823867 | ||
|
|
bba738f5f4 | ||
|
|
913cea0018 | ||
|
|
6602ce9960 | ||
|
|
f2864ebdf6 | ||
|
|
b93828cd33 | ||
|
|
cd3ca1ee25 | ||
|
|
6491577501 | ||
|
|
5c1043fe88 | ||
|
|
78637823c8 | ||
|
|
ee12ec36cc | ||
|
|
1b1dfa8893 | ||
|
|
a482ee1fe4 | ||
|
|
20daae4de9 | ||
|
|
bb619a64fe | ||
|
|
67954a93b2 | ||
|
|
0b44cebe00 | ||
|
|
21ab8fe89c | ||
|
|
fbbc0af7bc | ||
|
|
1999b7a6c5 | ||
|
|
fa5462d674 | ||
|
|
b3736d6c6c | ||
|
|
031ecb5cb0 | ||
|
|
2faeefb063 | ||
|
|
d384860c34 | ||
|
|
c417b0c15a | ||
|
|
e49a401c23 | ||
|
|
27f51f1234 | ||
|
|
ad0f563816 | ||
|
|
44330a618b | ||
|
|
4d312cbc65 | ||
|
|
6bac561789 | ||
|
|
31254009cc | ||
|
|
b27a55e802 | ||
|
|
cab0e30ceb | ||
|
|
112f96bae0 | ||
|
|
66b85c96f4 | ||
|
|
0513481ec8 | ||
|
|
f62762b4d1 | ||
|
|
27e0ed30fe | ||
|
|
4bbf0a46b5 | ||
|
|
de77894e59 | ||
|
|
5a9615a1f2 | ||
|
|
c3077ee067 | ||
|
|
ea596eb4a2 | ||
|
|
378605cd00 | ||
|
|
56e455c745 | ||
|
|
8d70a0e0bc | ||
|
|
b67e9e4aa8 | ||
|
|
44c3ad6810 | ||
|
|
69c05e5843 | ||
|
|
6ab91a5c88 | ||
|
|
759c7f5490 | ||
|
|
03b4a85825 | ||
|
|
737c4809e3 | ||
|
|
3657badb31 | ||
|
|
ef69a46e6a | ||
|
|
78106836be | ||
|
|
a7d1a88519 | ||
|
|
15ac3d8b0b | ||
|
|
08f888a326 | ||
|
|
ee9822fd42 | ||
|
|
17eb2f02f4 | ||
|
|
067419b7cd | ||
|
|
acfee42735 | ||
|
|
62966ac550 | ||
|
|
23c3c3cc0c | ||
|
|
0a65a37eed | ||
|
|
0df1005cb2 | ||
|
|
807b2d5c0a | ||
|
|
f65b28a189 | ||
|
|
a292e52b34 | ||
|
|
4555f8a9a6 | ||
|
|
0fea5564b6 | ||
|
|
476b7f43a2 | ||
|
|
d5d846f481 | ||
|
|
6fd356b654 | ||
|
|
48351d6da8 | ||
|
|
3068dad410 | ||
|
|
61d6838b2c | ||
|
|
e9acacdd5d | ||
|
|
3bb26fde2b | ||
|
|
7b7987363f | ||
|
|
3193ef0a89 | ||
|
|
a62298a635 | ||
|
|
6a7b6f6262 | ||
|
|
5b389bf5ca | ||
|
|
6da927a573 | ||
|
|
e6c7832485 | ||
|
|
c3073d6e74 | ||
|
|
b34582e6ba | ||
|
|
5136ffd10d | ||
|
|
cacb5bfe03 | ||
|
|
88fcb8aebb | ||
|
|
0dfd6facc1 | ||
|
|
5988941032 | ||
|
|
c6145a1101 | ||
|
|
2fb035fe1b | ||
|
|
f3be4cd2b3 | ||
|
|
15bb41d3c2 | ||
|
|
410990b2a3 | ||
|
|
51a2b74db9 | ||
|
|
bfc34939f2 | ||
|
|
90d6e550aa | ||
|
|
4f5af0ff9f | ||
|
|
ab21b518e1 | ||
|
|
d4e2adbdb2 | ||
|
|
85e0e8110a | ||
|
|
791e4269d2 | ||
|
|
3df5aa04b5 | ||
|
|
05d5c48d29 | ||
|
|
2b736b52d6 | ||
|
|
24954b909b | ||
|
|
ed5affe25a | ||
|
|
8591a151de | ||
|
|
b2a0e4b810 | ||
|
|
aee03fe2ef | ||
|
|
62702b8bd6 | ||
|
|
cfb36cc9a8 | ||
|
|
3b4575af3d | ||
|
|
dd67b0ee61 | ||
|
|
fb4a75c5aa | ||
|
|
060d46af42 | ||
|
|
83931e872b | ||
|
|
a8866b61a1 | ||
|
|
f733eddf0b | ||
|
|
ba99bc5fff | ||
|
|
e047e1eb15 | ||
|
|
c72478c74d | ||
|
|
544e56a71c | ||
|
|
bb5096075f | ||
|
|
80891daaed | ||
|
|
77a764e5bd | ||
|
|
74ed656bb9 | ||
|
|
82ce228433 | ||
|
|
d7ddb00e86 | ||
|
|
85796b3534 | ||
|
|
b582570022 | ||
|
|
8914bce3d8 | ||
|
|
6cc7c08ccf | ||
|
|
a1fb289290 | ||
|
|
572e844a91 | ||
|
|
906b9562de | ||
|
|
ef2553edf9 | ||
|
|
93242ca883 | ||
|
|
ee7aaf0ea4 | ||
|
|
1ae447e4fc | ||
|
|
5ecb022abc | ||
|
|
12ab3972de | ||
|
|
a1db5f0f0a | ||
|
|
5195d8a765 | ||
|
|
c10c21157d | ||
|
|
116a429c45 | ||
|
|
6c10783823 | ||
|
|
5f41036f4a | ||
|
|
e278688a4b |
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "15.20.4"
|
||||
__version__ = "15.23.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -360,45 +360,45 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
)
|
||||
|
||||
if not amount:
|
||||
return
|
||||
|
||||
gl_posting_date = end_date
|
||||
prev_posting_date = None
|
||||
# check if books nor frozen till endate:
|
||||
if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto):
|
||||
gl_posting_date = get_last_day(add_days(accounts_frozen_upto, 1))
|
||||
prev_posting_date = end_date
|
||||
|
||||
if via_journal_entry:
|
||||
book_revenue_via_journal_entry(
|
||||
doc,
|
||||
credit_account,
|
||||
debit_account,
|
||||
amount,
|
||||
base_amount,
|
||||
gl_posting_date,
|
||||
project,
|
||||
account_currency,
|
||||
item.cost_center,
|
||||
item,
|
||||
deferred_process,
|
||||
submit_journal_entry,
|
||||
)
|
||||
else:
|
||||
make_gl_entries(
|
||||
doc,
|
||||
credit_account,
|
||||
debit_account,
|
||||
against,
|
||||
amount,
|
||||
base_amount,
|
||||
gl_posting_date,
|
||||
project,
|
||||
account_currency,
|
||||
item.cost_center,
|
||||
item,
|
||||
deferred_process,
|
||||
)
|
||||
gl_posting_date = end_date
|
||||
prev_posting_date = None
|
||||
# check if books nor frozen till endate:
|
||||
if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto):
|
||||
gl_posting_date = get_last_day(add_days(accounts_frozen_upto, 1))
|
||||
prev_posting_date = end_date
|
||||
|
||||
if via_journal_entry:
|
||||
book_revenue_via_journal_entry(
|
||||
doc,
|
||||
credit_account,
|
||||
debit_account,
|
||||
amount,
|
||||
base_amount,
|
||||
gl_posting_date,
|
||||
project,
|
||||
account_currency,
|
||||
item.cost_center,
|
||||
item,
|
||||
deferred_process,
|
||||
submit_journal_entry,
|
||||
)
|
||||
else:
|
||||
make_gl_entries(
|
||||
doc,
|
||||
credit_account,
|
||||
debit_account,
|
||||
against,
|
||||
amount,
|
||||
base_amount,
|
||||
gl_posting_date,
|
||||
project,
|
||||
account_currency,
|
||||
item.cost_center,
|
||||
item,
|
||||
deferred_process,
|
||||
)
|
||||
|
||||
# Returned in case of any errors because it tries to submit the same record again and again in case of errors
|
||||
if frappe.flags.deferred_accounting_error:
|
||||
|
||||
@@ -57,9 +57,12 @@ frappe.ui.form.on("Accounting Dimension", {
|
||||
}
|
||||
},
|
||||
|
||||
label: function (frm) {
|
||||
frm.set_value("fieldname", frappe.model.scrub(frm.doc.label));
|
||||
},
|
||||
|
||||
document_type: function (frm) {
|
||||
frm.set_value("label", frm.doc.document_type);
|
||||
frm.set_value("fieldname", frappe.model.scrub(frm.doc.document_type));
|
||||
|
||||
frappe.db.get_value(
|
||||
"Accounting Dimension",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
},
|
||||
@@ -118,7 +119,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-03-07 11:02:24.535714",
|
||||
"modified": "2024-04-28 14:40:50.910884",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Reconciliation Tool",
|
||||
@@ -139,4 +140,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -719,7 +719,7 @@ def get_pe_matching_query(
|
||||
(ref_rank + amount_rank + party_rank + 1).as_("rank"),
|
||||
ConstantColumn("Payment Entry").as_("doctype"),
|
||||
pe.name,
|
||||
pe.paid_amount,
|
||||
pe.paid_amount_after_tax.as_("paid_amount"),
|
||||
pe.reference_no,
|
||||
pe.reference_date,
|
||||
pe.party,
|
||||
|
||||
@@ -219,12 +219,18 @@ def validate_expense_against_budget(args, expense_amount=0):
|
||||
def validate_budget_records(args, budget_records, expense_amount):
|
||||
for budget in budget_records:
|
||||
if flt(budget.budget_amount):
|
||||
amount = expense_amount or get_amount(args, budget)
|
||||
yearly_action, monthly_action = get_actions(args, budget)
|
||||
args["for_material_request"] = budget.for_material_request
|
||||
args["for_purchase_order"] = budget.for_purchase_order
|
||||
|
||||
if yearly_action in ("Stop", "Warn"):
|
||||
compare_expense_with_budget(
|
||||
args, flt(budget.budget_amount), _("Annual"), yearly_action, budget.budget_against, amount
|
||||
args,
|
||||
flt(budget.budget_amount),
|
||||
_("Annual"),
|
||||
yearly_action,
|
||||
budget.budget_against,
|
||||
expense_amount,
|
||||
)
|
||||
|
||||
if monthly_action in ["Stop", "Warn"]:
|
||||
@@ -240,18 +246,27 @@ def validate_budget_records(args, budget_records, expense_amount):
|
||||
_("Accumulated Monthly"),
|
||||
monthly_action,
|
||||
budget.budget_against,
|
||||
amount,
|
||||
expense_amount,
|
||||
)
|
||||
|
||||
|
||||
def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0):
|
||||
actual_expense = get_actual_expense(args)
|
||||
total_expense = actual_expense + amount
|
||||
args.actual_expense, args.requested_amount, args.ordered_amount = get_actual_expense(args), 0, 0
|
||||
if not amount:
|
||||
args.requested_amount, args.ordered_amount = get_requested_amount(args), get_ordered_amount(args)
|
||||
|
||||
if args.get("doctype") == "Material Request" and args.for_material_request:
|
||||
amount = args.requested_amount + args.ordered_amount
|
||||
|
||||
elif args.get("doctype") == "Purchase Order" and args.for_purchase_order:
|
||||
amount = args.ordered_amount
|
||||
|
||||
total_expense = args.actual_expense + amount
|
||||
|
||||
if total_expense > budget_amount:
|
||||
if actual_expense > budget_amount:
|
||||
if args.actual_expense > budget_amount:
|
||||
error_tense = _("is already")
|
||||
diff = actual_expense - budget_amount
|
||||
diff = args.actual_expense - budget_amount
|
||||
else:
|
||||
error_tense = _("will be")
|
||||
diff = total_expense - budget_amount
|
||||
@@ -268,6 +283,8 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_
|
||||
frappe.bold(fmt_money(diff, currency=currency)),
|
||||
)
|
||||
|
||||
msg += get_expense_breakup(args, currency, budget_against)
|
||||
|
||||
if frappe.flags.exception_approver_role and frappe.flags.exception_approver_role in frappe.get_roles(
|
||||
frappe.session.user
|
||||
):
|
||||
@@ -279,6 +296,83 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_
|
||||
frappe.msgprint(msg, indicator="orange", title=_("Budget Exceeded"))
|
||||
|
||||
|
||||
def get_expense_breakup(args, currency, budget_against):
|
||||
msg = "<hr>Total Expenses booked through - <ul>"
|
||||
|
||||
common_filters = frappe._dict(
|
||||
{
|
||||
args.budget_against_field: budget_against,
|
||||
"account": args.account,
|
||||
"company": args.company,
|
||||
}
|
||||
)
|
||||
|
||||
msg += (
|
||||
"<li>"
|
||||
+ frappe.utils.get_link_to_report(
|
||||
"General Ledger",
|
||||
label="Actual Expenses",
|
||||
filters=common_filters.copy().update(
|
||||
{
|
||||
"from_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_start_date"),
|
||||
"to_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_end_date"),
|
||||
"is_cancelled": 0,
|
||||
}
|
||||
),
|
||||
)
|
||||
+ " - "
|
||||
+ frappe.bold(fmt_money(args.actual_expense, currency=currency))
|
||||
+ "</li>"
|
||||
)
|
||||
|
||||
msg += (
|
||||
"<li>"
|
||||
+ frappe.utils.get_link_to_report(
|
||||
"Material Request",
|
||||
label="Material Requests",
|
||||
report_type="Report Builder",
|
||||
doctype="Material Request",
|
||||
filters=common_filters.copy().update(
|
||||
{
|
||||
"status": [["!=", "Stopped"]],
|
||||
"docstatus": 1,
|
||||
"material_request_type": "Purchase",
|
||||
"schedule_date": [["fiscal year", "2023-2024"]],
|
||||
"item_code": args.item_code,
|
||||
"per_ordered": [["<", 100]],
|
||||
}
|
||||
),
|
||||
)
|
||||
+ " - "
|
||||
+ frappe.bold(fmt_money(args.requested_amount, currency=currency))
|
||||
+ "</li>"
|
||||
)
|
||||
|
||||
msg += (
|
||||
"<li>"
|
||||
+ frappe.utils.get_link_to_report(
|
||||
"Purchase Order",
|
||||
label="Unbilled Orders",
|
||||
report_type="Report Builder",
|
||||
doctype="Purchase Order",
|
||||
filters=common_filters.copy().update(
|
||||
{
|
||||
"status": [["!=", "Closed"]],
|
||||
"docstatus": 1,
|
||||
"transaction_date": [["fiscal year", "2023-2024"]],
|
||||
"item_code": args.item_code,
|
||||
"per_billed": [["<", 100]],
|
||||
}
|
||||
),
|
||||
)
|
||||
+ " - "
|
||||
+ frappe.bold(fmt_money(args.ordered_amount, currency=currency))
|
||||
+ "</li></ul>"
|
||||
)
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def get_actions(args, budget):
|
||||
yearly_action = budget.action_if_annual_budget_exceeded
|
||||
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded
|
||||
@@ -294,23 +388,9 @@ def get_actions(args, budget):
|
||||
return yearly_action, monthly_action
|
||||
|
||||
|
||||
def get_amount(args, budget):
|
||||
amount = 0
|
||||
|
||||
if args.get("doctype") == "Material Request" and budget.for_material_request:
|
||||
amount = (
|
||||
get_requested_amount(args, budget) + get_ordered_amount(args, budget) + get_actual_expense(args)
|
||||
)
|
||||
|
||||
elif args.get("doctype") == "Purchase Order" and budget.for_purchase_order:
|
||||
amount = get_ordered_amount(args, budget) + get_actual_expense(args)
|
||||
|
||||
return amount
|
||||
|
||||
|
||||
def get_requested_amount(args, budget):
|
||||
def get_requested_amount(args):
|
||||
item_code = args.get("item_code")
|
||||
condition = get_other_condition(args, budget, "Material Request")
|
||||
condition = get_other_condition(args, "Material Request")
|
||||
|
||||
data = frappe.db.sql(
|
||||
""" select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount
|
||||
@@ -324,9 +404,9 @@ def get_requested_amount(args, budget):
|
||||
return data[0][0] if data else 0
|
||||
|
||||
|
||||
def get_ordered_amount(args, budget):
|
||||
def get_ordered_amount(args):
|
||||
item_code = args.get("item_code")
|
||||
condition = get_other_condition(args, budget, "Purchase Order")
|
||||
condition = get_other_condition(args, "Purchase Order")
|
||||
|
||||
data = frappe.db.sql(
|
||||
f""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount
|
||||
@@ -340,7 +420,7 @@ def get_ordered_amount(args, budget):
|
||||
return data[0][0] if data else 0
|
||||
|
||||
|
||||
def get_other_condition(args, budget, for_doc):
|
||||
def get_other_condition(args, for_doc):
|
||||
condition = "expense_account = '%s'" % (args.expense_account)
|
||||
budget_against_field = args.get("budget_against_field")
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2022-01-31 13:22:58.916273",
|
||||
"modified": "2024-04-24 10:55:54.083042",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Cost Center",
|
||||
@@ -163,6 +163,15 @@
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Purchase User"
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"report": 1,
|
||||
"role": "Employee",
|
||||
"select": 1,
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "parent_cost_center, is_group",
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
"module": "Accounts",
|
||||
"name": "Fiscal Year",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
@@ -119,6 +119,14 @@
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Employee"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Stock Manager"
|
||||
}
|
||||
],
|
||||
"show_name_in_global_search": 1,
|
||||
|
||||
@@ -6,7 +6,7 @@ import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint, scrub
|
||||
from frappe.utils import cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate
|
||||
from frappe.utils import comma_and, cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts
|
||||
@@ -146,6 +146,7 @@ class JournalEntry(AccountsController):
|
||||
self.validate_empty_accounts_table()
|
||||
self.validate_inter_company_accounts()
|
||||
self.validate_depr_entry_voucher_type()
|
||||
self.validate_advance_accounts()
|
||||
|
||||
if self.docstatus == 0:
|
||||
self.apply_tax_withholding()
|
||||
@@ -153,6 +154,20 @@ class JournalEntry(AccountsController):
|
||||
if not self.title:
|
||||
self.title = self.get_title()
|
||||
|
||||
def validate_advance_accounts(self):
|
||||
journal_accounts = set([x.account for x in self.accounts])
|
||||
advance_accounts = set()
|
||||
advance_accounts.add(
|
||||
frappe.get_cached_value("Company", self.company, "default_advance_received_account")
|
||||
)
|
||||
advance_accounts.add(frappe.get_cached_value("Company", self.company, "default_advance_paid_account"))
|
||||
if advance_accounts_used := journal_accounts & advance_accounts:
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Making Journal Entries against advance accounts: {0} is not recommended. These Journals won't be available for Reconciliation."
|
||||
).format(frappe.bold(comma_and(advance_accounts_used)))
|
||||
)
|
||||
|
||||
def validate_for_repost(self):
|
||||
validate_docs_for_voucher_types(["Journal Entry"])
|
||||
validate_docs_for_deferred_accounting([self.name], [])
|
||||
|
||||
@@ -1334,7 +1334,9 @@ frappe.ui.form.on("Payment Entry", {
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
frm.set_value(field, r.message.account);
|
||||
if (!frm.doc.mode_of_payment) {
|
||||
frm.set_value(field, r.message.account);
|
||||
}
|
||||
frm.set_value("bank", r.message.bank);
|
||||
frm.set_value("bank_account_no", r.message.bank_account_no);
|
||||
}
|
||||
|
||||
@@ -150,6 +150,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
@@ -476,6 +477,7 @@
|
||||
"label": "More Information"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
@@ -776,7 +778,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2024-01-03 12:46:41.759121",
|
||||
"modified": "2024-04-11 11:25:07.366347",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
||||
@@ -76,6 +76,7 @@ class PaymentEntry(AccountsController):
|
||||
self.setup_party_account_field()
|
||||
self.set_missing_values()
|
||||
self.set_liability_account()
|
||||
self.validate_advance_account_currency()
|
||||
self.set_missing_ref_details(force=True)
|
||||
self.validate_payment_type()
|
||||
self.validate_party_details()
|
||||
@@ -113,6 +114,7 @@ class PaymentEntry(AccountsController):
|
||||
if self.docstatus > 0 or self.payment_type == "Internal Transfer":
|
||||
return
|
||||
|
||||
self.book_advance_payments_in_separate_party_account = False
|
||||
if self.party_type not in ("Customer", "Supplier"):
|
||||
return
|
||||
|
||||
@@ -157,6 +159,22 @@ class PaymentEntry(AccountsController):
|
||||
alert=True,
|
||||
)
|
||||
|
||||
def validate_advance_account_currency(self):
|
||||
if self.book_advance_payments_in_separate_party_account is True:
|
||||
company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
if self.payment_type == "Receive" and self.paid_from_account_currency != company_currency:
|
||||
frappe.throw(
|
||||
_("Booking advances in foreign currency account: {0} ({1}) is not yet supported.").format(
|
||||
frappe.bold(self.paid_from), frappe.bold(self.paid_from_account_currency)
|
||||
)
|
||||
)
|
||||
if self.payment_type == "Pay" and self.paid_to_account_currency != company_currency:
|
||||
frappe.throw(
|
||||
_("Booking advances in foreign currency account: {0} ({1}) is not yet supported.").format(
|
||||
frappe.bold(self.paid_to), frappe.bold(self.paid_to_account_currency)
|
||||
)
|
||||
)
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = (
|
||||
"GL Entry",
|
||||
@@ -1113,88 +1131,71 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
|
||||
for d in self.get("references"):
|
||||
# re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
cost_center = self.cost_center
|
||||
if d.reference_doctype == "Sales Invoice" and not cost_center:
|
||||
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
|
||||
if self.payment_type == "Receive":
|
||||
amount = self.base_paid_amount
|
||||
else:
|
||||
amount = self.base_received_amount
|
||||
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d)
|
||||
reverse_dr_or_cr = 0
|
||||
|
||||
if d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
is_return = frappe.db.get_value(d.reference_doctype, d.reference_name, "is_return")
|
||||
payable_party_types = get_party_types_from_account_type("Payable")
|
||||
receivable_party_types = get_party_types_from_account_type("Receivable")
|
||||
if (
|
||||
is_return
|
||||
and self.party_type in receivable_party_types
|
||||
and (self.payment_type == "Pay")
|
||||
):
|
||||
reverse_dr_or_cr = 1
|
||||
elif (
|
||||
is_return
|
||||
and self.party_type in payable_party_types
|
||||
and (self.payment_type == "Receive")
|
||||
):
|
||||
reverse_dr_or_cr = 1
|
||||
|
||||
if is_return and not reverse_dr_or_cr:
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
exchange_rate = self.get_exchange_rate()
|
||||
amount_in_account_currency = amount * exchange_rate
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr: amount,
|
||||
dr_or_cr + "_in_account_currency": amount_in_account_currency,
|
||||
"against_voucher_type": "Payment Entry",
|
||||
"against_voucher": self.name,
|
||||
"cost_center": self.cost_center,
|
||||
dr_or_cr: abs(allocated_amount_in_company_currency),
|
||||
dr_or_cr + "_in_account_currency": abs(d.allocated_amount),
|
||||
"against_voucher_type": d.reference_doctype,
|
||||
"against_voucher": d.reference_name,
|
||||
"cost_center": cost_center,
|
||||
}
|
||||
)
|
||||
gl_entries.append(gle)
|
||||
else:
|
||||
for d in self.get("references"):
|
||||
# re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
cost_center = self.cost_center
|
||||
if d.reference_doctype == "Sales Invoice" and not cost_center:
|
||||
cost_center = frappe.db.get_value(
|
||||
d.reference_doctype, d.reference_name, "cost_center"
|
||||
)
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
if self.unallocated_amount:
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
exchange_rate = self.get_exchange_rate()
|
||||
base_unallocated_amount = self.unallocated_amount * exchange_rate
|
||||
|
||||
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(
|
||||
d
|
||||
)
|
||||
reverse_dr_or_cr = 0
|
||||
|
||||
if d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
is_return = frappe.db.get_value(d.reference_doctype, d.reference_name, "is_return")
|
||||
payable_party_types = get_party_types_from_account_type("Payable")
|
||||
receivable_party_types = get_party_types_from_account_type("Receivable")
|
||||
if (
|
||||
is_return
|
||||
and self.party_type in receivable_party_types
|
||||
and (self.payment_type == "Pay")
|
||||
):
|
||||
reverse_dr_or_cr = 1
|
||||
elif (
|
||||
is_return
|
||||
and self.party_type in payable_party_types
|
||||
and (self.payment_type == "Receive")
|
||||
):
|
||||
reverse_dr_or_cr = 1
|
||||
|
||||
if is_return and not reverse_dr_or_cr:
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
gle = party_gl_dict.copy()
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
||||
dr_or_cr: base_unallocated_amount,
|
||||
}
|
||||
)
|
||||
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr: abs(allocated_amount_in_company_currency),
|
||||
dr_or_cr + "_in_account_currency": abs(d.allocated_amount),
|
||||
"against_voucher_type": d.reference_doctype,
|
||||
"against_voucher": d.reference_name,
|
||||
"cost_center": cost_center,
|
||||
"against_voucher_type": "Payment Entry",
|
||||
"against_voucher": self.name,
|
||||
}
|
||||
)
|
||||
gl_entries.append(gle)
|
||||
|
||||
if self.unallocated_amount:
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
exchange_rate = self.get_exchange_rate()
|
||||
base_unallocated_amount = self.unallocated_amount * exchange_rate
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
||||
dr_or_cr: base_unallocated_amount,
|
||||
}
|
||||
)
|
||||
|
||||
gl_entries.append(gle)
|
||||
gl_entries.append(gle)
|
||||
|
||||
def make_advance_gl_entries(
|
||||
self, entry: object | dict = None, cancel: bool = 0, update_outstanding: str = "Yes"
|
||||
@@ -1209,7 +1210,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def add_advance_gl_entries(self, gl_entries: list, entry: object | dict | None):
|
||||
"""
|
||||
If 'entry' is passed, GL enties only for that reference is added.
|
||||
If 'entry' is passed, GL entries only for that reference is added.
|
||||
"""
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
references = [x for x in self.get("references")]
|
||||
@@ -1221,8 +1222,6 @@ class PaymentEntry(AccountsController):
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Journal Entry",
|
||||
"Sales Order",
|
||||
"Purchase Order",
|
||||
"Payment Entry",
|
||||
):
|
||||
self.add_advance_gl_for_reference(gl_entries, ref)
|
||||
@@ -2028,6 +2027,8 @@ def get_negative_outstanding_invoices(
|
||||
@frappe.whitelist()
|
||||
def get_party_details(company, party_type, party, date, cost_center=None):
|
||||
bank_account = ""
|
||||
party_bank_account = ""
|
||||
|
||||
if not frappe.db.exists(party_type, party):
|
||||
frappe.throw(_("{0} {1} does not exist").format(_(party_type), party))
|
||||
|
||||
@@ -2039,8 +2040,8 @@ def get_party_details(company, party_type, party, date, cost_center=None):
|
||||
party_balance = get_balance_on(party_type=party_type, party=party, cost_center=cost_center)
|
||||
if party_type in ["Customer", "Supplier"]:
|
||||
party_bank_account = get_party_bank_account(party_type, party)
|
||||
bank_account = get_default_company_bank_account(company, party_type, party)
|
||||
|
||||
bank_account = get_default_company_bank_account(company, party_type, party)
|
||||
return {
|
||||
"party_account": party_account,
|
||||
"party_name": party_name,
|
||||
|
||||
@@ -10,6 +10,7 @@ from frappe.utils import add_days, flt, nowdate
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
get_outstanding_reference_documents,
|
||||
get_party_details,
|
||||
get_payment_entry,
|
||||
get_reference_details,
|
||||
)
|
||||
@@ -1439,6 +1440,68 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
self.check_gl_entries()
|
||||
self.check_pl_entries()
|
||||
|
||||
def test_advance_as_liability_against_order(self):
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import (
|
||||
make_purchase_invoice as _make_purchase_invoice,
|
||||
)
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
|
||||
company = "_Test Company"
|
||||
|
||||
advance_account = create_account(
|
||||
parent_account="Current Liabilities - _TC",
|
||||
account_name="Advances Paid",
|
||||
company=company,
|
||||
account_type="Liability",
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
company,
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_paid_account": advance_account,
|
||||
},
|
||||
)
|
||||
|
||||
po = create_purchase_order(supplier="_Test Supplier")
|
||||
pe = get_payment_entry("Purchase Order", po.name, bank_account="Cash - _TC")
|
||||
pe.save().submit()
|
||||
|
||||
pre_reconciliation_gle = [
|
||||
{"account": "Cash - _TC", "debit": 0.0, "credit": 5000.0},
|
||||
{"account": advance_account, "debit": 5000.0, "credit": 0.0},
|
||||
]
|
||||
|
||||
self.voucher_no = pe.name
|
||||
self.expected_gle = pre_reconciliation_gle
|
||||
self.check_gl_entries()
|
||||
|
||||
# Make Purchase Invoice against the order
|
||||
pi = _make_purchase_invoice(po.name)
|
||||
pi.append(
|
||||
"advances",
|
||||
{
|
||||
"reference_type": pe.doctype,
|
||||
"reference_name": pe.name,
|
||||
"reference_row": pe.references[0].name,
|
||||
"advance_amount": 5000,
|
||||
"allocated_amount": 5000,
|
||||
},
|
||||
)
|
||||
pi.save().submit()
|
||||
|
||||
# # assert General and Payment Ledger entries post partial reconciliation
|
||||
self.expected_gle = [
|
||||
{"account": pi.credit_to, "debit": 5000.0, "credit": 0.0},
|
||||
{"account": "Cash - _TC", "debit": 0.0, "credit": 5000.0},
|
||||
{"account": advance_account, "debit": 5000.0, "credit": 0.0},
|
||||
{"account": advance_account, "debit": 0.0, "credit": 5000.0},
|
||||
]
|
||||
|
||||
self.voucher_no = pe.name
|
||||
self.check_gl_entries()
|
||||
|
||||
def check_pl_entries(self):
|
||||
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||
pl_entries = (
|
||||
@@ -1684,6 +1747,10 @@ def create_payment_entry(**args):
|
||||
payment_entry.reference_no = "Test001"
|
||||
payment_entry.reference_date = nowdate()
|
||||
|
||||
get_party_details(
|
||||
payment_entry.company, payment_entry.party_type, payment_entry.party, payment_entry.posting_date
|
||||
)
|
||||
|
||||
if args.get("save"):
|
||||
payment_entry.save()
|
||||
if args.get("submit"):
|
||||
|
||||
@@ -71,6 +71,7 @@ frappe.ui.form.on("Payment Order", {
|
||||
target: frm,
|
||||
date_field: "posting_date",
|
||||
setters: {
|
||||
party_type: "Supplier",
|
||||
party: frm.doc.supplier || "",
|
||||
},
|
||||
get_query_filters: {
|
||||
@@ -91,6 +92,7 @@ frappe.ui.form.on("Payment Order", {
|
||||
source_doctype: "Payment Request",
|
||||
target: frm,
|
||||
setters: {
|
||||
party_type: "Supplier",
|
||||
party: frm.doc.supplier || "",
|
||||
},
|
||||
get_query_filters: {
|
||||
|
||||
@@ -176,8 +176,12 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
},
|
||||
callback: (r) => {
|
||||
if (!r.exc && r.message) {
|
||||
this.frm.set_value("receivable_payable_account", r.message[0]);
|
||||
this.frm.set_value("default_advance_account", r.message[1]);
|
||||
if (typeof r.message === "string") {
|
||||
this.frm.set_value("receivable_payable_account", r.message);
|
||||
} else if (Array.isArray(r.message)) {
|
||||
this.frm.set_value("receivable_payable_account", r.message[0]);
|
||||
this.frm.set_value("default_advance_account", r.message[1]);
|
||||
}
|
||||
}
|
||||
this.frm.refresh();
|
||||
},
|
||||
|
||||
@@ -195,6 +195,8 @@
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.party",
|
||||
"description": "Only 'Payment Entries' made against this advance account are supported.",
|
||||
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account",
|
||||
"fieldname": "default_advance_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Advance Account",
|
||||
@@ -229,7 +231,7 @@
|
||||
"is_virtual": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-14 13:38:16.264013",
|
||||
"modified": "2024-04-23 12:38:29.557315",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation",
|
||||
|
||||
@@ -573,6 +573,22 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
|
||||
if pricing_rule.apply_discount_on_rate and item_details.get("discount_percentage"):
|
||||
# Apply discount on discounted rate
|
||||
item_details[field] += (100 - item_details[field]) * (pricing_rule.get(field, 0) / 100)
|
||||
elif args.price_list_rate:
|
||||
value = pricing_rule.get(field, 0)
|
||||
calculate_discount_percentage = False
|
||||
if field == "discount_percentage":
|
||||
field = "discount_amount"
|
||||
value = args.price_list_rate * (value / 100)
|
||||
calculate_discount_percentage = True
|
||||
|
||||
if field not in item_details:
|
||||
item_details.setdefault(field, 0)
|
||||
|
||||
item_details[field] += value if pricing_rule else args.get(field, 0)
|
||||
if calculate_discount_percentage and args.price_list_rate and item_details.discount_amount:
|
||||
item_details.discount_percentage = flt(
|
||||
(flt(item_details.discount_amount) / flt(args.price_list_rate)) * 100
|
||||
)
|
||||
else:
|
||||
if field not in item_details:
|
||||
item_details.setdefault(field, 0)
|
||||
|
||||
@@ -1102,7 +1102,60 @@ class TestPricingRule(unittest.TestCase):
|
||||
so.load_from_db()
|
||||
self.assertEqual(so.items[1].is_free_item, 1)
|
||||
self.assertEqual(so.items[1].item_code, "_Test Item")
|
||||
self.assertEqual(so.items[1].qty, 4)
|
||||
self.assertEqual(so.items[1].qty, 3)
|
||||
|
||||
def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule 1",
|
||||
"name": "_Test Pricing Rule 1",
|
||||
"apply_on": "Item Code",
|
||||
"currency": "USD",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"price_or_product_discount": "Price",
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"discount_percentage": 10,
|
||||
"apply_multiple_pricing_rules": 1,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
|
||||
frappe.get_doc(test_record.copy()).insert()
|
||||
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule 2",
|
||||
"name": "_Test Pricing Rule 2",
|
||||
"apply_on": "Item Code",
|
||||
"currency": "USD",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"price_or_product_discount": "Price",
|
||||
"rate_or_discount": "Discount Amount",
|
||||
"discount_amount": 100,
|
||||
"apply_multiple_pricing_rules": 1,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
|
||||
frappe.get_doc(test_record.copy()).insert()
|
||||
|
||||
so = make_sales_order(item_code="_Test Item", qty=1, price_list_rate=1000, do_not_submit=True)
|
||||
self.assertEqual(so.items[0].discount_amount, 200)
|
||||
self.assertEqual(so.items[0].rate, 800)
|
||||
|
||||
frappe.delete_doc_if_exists("Sales Order", so.name)
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
|
||||
|
||||
test_dependencies = ["Campaign"]
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import copy
|
||||
import json
|
||||
import math
|
||||
|
||||
import frappe
|
||||
from frappe import _, bold
|
||||
@@ -653,7 +654,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
|
||||
if transaction_qty:
|
||||
qty = flt(transaction_qty) * qty / pricing_rule.recurse_for
|
||||
if pricing_rule.round_free_qty:
|
||||
qty = round(qty)
|
||||
qty = math.floor(qty)
|
||||
|
||||
free_item_data_args = {
|
||||
"item_code": free_item,
|
||||
|
||||
@@ -11,13 +11,15 @@
|
||||
{
|
||||
"fieldname": "cost_center_name",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
"options": "Cost Center",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-08-03 16:56:45.744905",
|
||||
"modified": "2024-05-03 17:16:51.666461",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "PSOA Cost Center",
|
||||
@@ -27,4 +29,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ class PSOACostCenter(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
cost_center_name: DF.Link | None
|
||||
cost_center_name: DF.Link
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
||||
@@ -299,6 +299,7 @@
|
||||
"remember_last_selected_value": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
@@ -1367,6 +1368,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
@@ -1637,7 +1639,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-20 15:57:00.736868",
|
||||
"modified": "2024-04-11 11:28:42.802211",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -68,15 +68,11 @@ class PurchaseInvoice(BuyingController):
|
||||
from erpnext.accounts.doctype.purchase_invoice_advance.purchase_invoice_advance import (
|
||||
PurchaseInvoiceAdvance,
|
||||
)
|
||||
from erpnext.accounts.doctype.purchase_invoice_item.purchase_invoice_item import (
|
||||
PurchaseInvoiceItem,
|
||||
)
|
||||
from erpnext.accounts.doctype.purchase_invoice_item.purchase_invoice_item import PurchaseInvoiceItem
|
||||
from erpnext.accounts.doctype.purchase_taxes_and_charges.purchase_taxes_and_charges import (
|
||||
PurchaseTaxesandCharges,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_withheld_vouchers.tax_withheld_vouchers import (
|
||||
TaxWithheldVouchers,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_withheld_vouchers.tax_withheld_vouchers import TaxWithheldVouchers
|
||||
from erpnext.buying.doctype.purchase_receipt_item_supplied.purchase_receipt_item_supplied import (
|
||||
PurchaseReceiptItemSupplied,
|
||||
)
|
||||
@@ -1095,7 +1091,7 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
|
||||
# check if the exchange rate has changed
|
||||
if item.get("purchase_receipt"):
|
||||
if item.get("purchase_receipt") and self.auto_accounting_for_stock:
|
||||
if (
|
||||
exchange_rate_map[item.purchase_receipt]
|
||||
and self.conversion_rate != exchange_rate_map[item.purchase_receipt]
|
||||
|
||||
@@ -289,6 +289,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"hide_days": 1,
|
||||
@@ -354,6 +355,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"hide_days": 1,
|
||||
@@ -2185,7 +2187,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2024-03-22 17:50:34.395602",
|
||||
"modified": "2024-04-11 11:30:26.272441",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
@@ -2240,4 +2242,4 @@
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,13 +55,9 @@ class SalesInvoice(SellingController):
|
||||
|
||||
from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule
|
||||
from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail
|
||||
from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import (
|
||||
SalesInvoiceAdvance,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import SalesInvoiceAdvance
|
||||
from erpnext.accounts.doctype.sales_invoice_item.sales_invoice_item import SalesInvoiceItem
|
||||
from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import (
|
||||
SalesInvoicePayment,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import SalesInvoicePayment
|
||||
from erpnext.accounts.doctype.sales_invoice_timesheet.sales_invoice_timesheet import (
|
||||
SalesInvoiceTimesheet,
|
||||
)
|
||||
@@ -392,6 +388,9 @@ class SalesInvoice(SellingController):
|
||||
validate_account_head(item.idx, item.income_account, self.company, "Income")
|
||||
|
||||
def set_tax_withholding(self):
|
||||
if self.get("is_opening") == "Yes":
|
||||
return
|
||||
|
||||
tax_withholding_details = get_party_tax_withholding_details(self)
|
||||
|
||||
if not tax_withholding_details:
|
||||
|
||||
@@ -1766,6 +1766,49 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
self.assertTrue(gle)
|
||||
|
||||
def test_gle_in_transaction_currency(self):
|
||||
# create multi currency sales invoice with 2 items with same income account
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
debit_to="_Test Receivable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=50,
|
||||
do_not_submit=True,
|
||||
)
|
||||
# add 2nd item with same income account
|
||||
si.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"qty": 1,
|
||||
"rate": 80,
|
||||
"income_account": "Sales - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
},
|
||||
)
|
||||
si.submit()
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
"""select transaction_currency, transaction_exchange_rate,
|
||||
debit_in_transaction_currency, credit_in_transaction_currency
|
||||
from `tabGL Entry`
|
||||
where voucher_type='Sales Invoice' and voucher_no=%s and account = 'Sales - _TC'
|
||||
order by account asc""",
|
||||
si.name,
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
expected_gle = {
|
||||
"transaction_currency": "USD",
|
||||
"transaction_exchange_rate": 50,
|
||||
"debit_in_transaction_currency": 0,
|
||||
"credit_in_transaction_currency": 180,
|
||||
}
|
||||
|
||||
for gle in gl_entries:
|
||||
for field in expected_gle:
|
||||
self.assertEqual(expected_gle[field], gle[field])
|
||||
|
||||
def test_invoice_exchange_rate(self):
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
|
||||
@@ -112,11 +112,7 @@ class Subscription(Document):
|
||||
"""
|
||||
_current_invoice_start = None
|
||||
|
||||
if (
|
||||
self.is_new_subscription()
|
||||
and self.trial_period_end
|
||||
and getdate(self.trial_period_end) > getdate(self.start_date)
|
||||
):
|
||||
if self.trial_period_end and getdate(self.trial_period_end) > getdate(self.start_date):
|
||||
_current_invoice_start = add_days(self.trial_period_end, 1)
|
||||
elif self.trial_period_start and self.is_trialling():
|
||||
_current_invoice_start = self.trial_period_start
|
||||
@@ -143,7 +139,7 @@ class Subscription(Document):
|
||||
else:
|
||||
billing_cycle_info = self.get_billing_cycle_data()
|
||||
if billing_cycle_info:
|
||||
if self.is_new_subscription() and getdate(self.start_date) < getdate(date):
|
||||
if getdate(self.start_date) < getdate(date):
|
||||
_current_invoice_end = add_to_date(self.start_date, **billing_cycle_info)
|
||||
|
||||
# For cases where trial period is for an entire billing interval
|
||||
@@ -234,14 +230,14 @@ class Subscription(Document):
|
||||
self.cancelation_date = getdate(posting_date) if self.status == "Cancelled" else None
|
||||
elif self.current_invoice_is_past_due() and not self.is_past_grace_period():
|
||||
self.status = "Past Due Date"
|
||||
elif not self.has_outstanding_invoice() or self.is_new_subscription():
|
||||
elif not self.has_outstanding_invoice():
|
||||
self.status = "Active"
|
||||
|
||||
def is_trialling(self) -> bool:
|
||||
"""
|
||||
Returns `True` if the `Subscription` is in trial period.
|
||||
"""
|
||||
return not self.period_has_passed(self.trial_period_end) and self.is_new_subscription()
|
||||
return not self.period_has_passed(self.trial_period_end)
|
||||
|
||||
@staticmethod
|
||||
def period_has_passed(
|
||||
@@ -288,14 +284,6 @@ class Subscription(Document):
|
||||
def invoice_document_type(self) -> str:
|
||||
return "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
|
||||
def is_new_subscription(self) -> bool:
|
||||
"""
|
||||
Returns `True` if `Subscription` has never generated an invoice
|
||||
"""
|
||||
return self.is_new() or not frappe.db.exists(
|
||||
{"doctype": self.invoice_document_type, "subscription": self.name}
|
||||
)
|
||||
|
||||
def validate(self) -> None:
|
||||
self.validate_trial_period()
|
||||
self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval())
|
||||
@@ -604,7 +592,7 @@ class Subscription(Document):
|
||||
return False
|
||||
|
||||
if self.generate_invoice_at == "Beginning of the current subscription period" and (
|
||||
getdate(posting_date) == getdate(self.current_invoice_start) or self.is_new_subscription()
|
||||
getdate(posting_date) == getdate(self.current_invoice_start)
|
||||
):
|
||||
return True
|
||||
elif self.generate_invoice_at == "Days before the current subscription period" and (
|
||||
|
||||
@@ -445,11 +445,11 @@ class TestSubscription(FrappeTestCase):
|
||||
|
||||
# Process subscription and create first invoice
|
||||
# Subscription status will be unpaid since due date has already passed
|
||||
subscription.process()
|
||||
subscription.process(posting_date="2018-01-01")
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
self.assertEqual(subscription.status, "Unpaid")
|
||||
|
||||
subscription.process()
|
||||
subscription.process(posting_date="2018-04-01")
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
|
||||
def test_multi_currency_subscription(self):
|
||||
@@ -462,7 +462,7 @@ class TestSubscription(FrappeTestCase):
|
||||
party=party,
|
||||
)
|
||||
|
||||
subscription.process()
|
||||
subscription.process(posting_date="2018-01-01")
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
self.assertEqual(subscription.status, "Unpaid")
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_user_permissions": 1,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
@@ -53,7 +53,7 @@
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_user_permissions": 1,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
@@ -87,7 +87,7 @@
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-04-13 18:44:25.055382",
|
||||
"modified": "2024-04-30 10:26:48.21829",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withholding Account",
|
||||
|
||||
@@ -9,6 +9,8 @@ from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Abs, Sum
|
||||
from frappe.utils import cint, flt, getdate
|
||||
|
||||
from erpnext.controllers.accounts_controller import validate_account_head
|
||||
|
||||
|
||||
class TaxWithholdingCategory(Document):
|
||||
# begin: auto-generated types
|
||||
@@ -53,6 +55,7 @@ class TaxWithholdingCategory(Document):
|
||||
if d.get("account") in existing_accounts:
|
||||
frappe.throw(_("Account {0} added multiple times").format(frappe.bold(d.get("account"))))
|
||||
|
||||
validate_account_head(d.idx, d.get("account"), d.get("company"))
|
||||
existing_accounts.append(d.get("account"))
|
||||
|
||||
def validate_thresholds(self):
|
||||
@@ -282,6 +285,14 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
if taxable_vouchers:
|
||||
tax_deducted = get_deducted_tax(taxable_vouchers, tax_details)
|
||||
|
||||
# If advance is outside the current tax withholding period (usually a fiscal year), `get_deducted_tax` won't fetch it.
|
||||
# updating `tax_deducted` with correct advance tax value (from current and previous previous withholding periods), will allow the
|
||||
# rest of the below logic to function properly
|
||||
# ---FY 2023-------------||---------------------FY 2024-----------------------||--
|
||||
# ---Advance-------------||---------Inv_1--------Inv_2------------------------||--
|
||||
if tax_deducted_on_advances:
|
||||
tax_deducted += get_advance_tax_across_fiscal_year(tax_deducted_on_advances, tax_details)
|
||||
|
||||
tax_amount = 0
|
||||
|
||||
if party_type == "Supplier":
|
||||
@@ -418,7 +429,7 @@ def get_taxes_deducted_on_advances_allocated(inv, tax_details):
|
||||
frappe.qb.from_(at)
|
||||
.inner_join(pe)
|
||||
.on(pe.name == at.parent)
|
||||
.select(at.parent, at.name, at.tax_amount, at.allocated_amount)
|
||||
.select(pe.posting_date, at.parent, at.name, at.tax_amount, at.allocated_amount)
|
||||
.where(pe.tax_withholding_category == tax_details.get("tax_withholding_category"))
|
||||
.where(at.parent.isin(advances))
|
||||
.where(at.account_head == tax_details.account_head)
|
||||
@@ -443,6 +454,16 @@ def get_deducted_tax(taxable_vouchers, tax_details):
|
||||
return sum(entries)
|
||||
|
||||
|
||||
def get_advance_tax_across_fiscal_year(tax_deducted_on_advances, tax_details):
|
||||
"""
|
||||
Only applies for Taxes deducted on Advance Payments
|
||||
"""
|
||||
advance_tax_from_across_fiscal_year = sum(
|
||||
[adv.tax_amount for adv in tax_deducted_on_advances if adv.posting_date < tax_details.from_date]
|
||||
)
|
||||
return advance_tax_from_across_fiscal_year
|
||||
|
||||
|
||||
def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
tds_amount = 0
|
||||
invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import datetime
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
from frappe.utils import today
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
|
||||
|
||||
test_dependencies = ["Supplier Group", "Customer Group"]
|
||||
|
||||
|
||||
class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
# create relevant supplier, etc
|
||||
@@ -21,7 +25,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
make_pan_no_field()
|
||||
|
||||
def tearDown(self):
|
||||
cancel_invoices()
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_cumulative_threshold_tds(self):
|
||||
frappe.db.set_value(
|
||||
@@ -317,8 +321,6 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
d.cancel()
|
||||
|
||||
def test_tds_deduction_for_po_via_payment_entry(self):
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
|
||||
frappe.db.set_value(
|
||||
"Supplier", "Test TDS Supplier8", "tax_withholding_category", "Cumulative Threshold TDS"
|
||||
)
|
||||
@@ -485,6 +487,133 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
pi2.cancel()
|
||||
pi3.cancel()
|
||||
|
||||
def set_previous_fy_and_tax_category(self):
|
||||
test_company = "_Test Company"
|
||||
category = "Cumulative Threshold TDS"
|
||||
|
||||
def add_company_to_fy(fy, company):
|
||||
if not [x.company for x in fy.companies if x.company == company]:
|
||||
fy.append("companies", {"company": company})
|
||||
fy.save()
|
||||
|
||||
# setup previous fiscal year
|
||||
fiscal_year = get_fiscal_year(today(), company=test_company)
|
||||
if prev_fiscal_year := get_fiscal_year(add_days(fiscal_year[1], -10)):
|
||||
self.prev_fy = frappe.get_doc("Fiscal Year", prev_fiscal_year[0])
|
||||
add_company_to_fy(self.prev_fy, test_company)
|
||||
else:
|
||||
# make previous fiscal year
|
||||
start = datetime.date(fiscal_year[1].year - 1, fiscal_year[1].month, fiscal_year[1].day)
|
||||
end = datetime.date(fiscal_year[2].year - 1, fiscal_year[2].month, fiscal_year[2].day)
|
||||
self.prev_fy = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year_start_date": start,
|
||||
"year_end_date": end,
|
||||
"companies": [{"company": test_company}],
|
||||
}
|
||||
)
|
||||
self.prev_fy.save()
|
||||
|
||||
# setup tax withholding category for previous fiscal year
|
||||
cat = frappe.get_doc("Tax Withholding Category", category)
|
||||
cat.append(
|
||||
"rates",
|
||||
{
|
||||
"from_date": self.prev_fy.year_start_date,
|
||||
"to_date": self.prev_fy.year_end_date,
|
||||
"tax_withholding_rate": 10,
|
||||
"single_threshold": 0,
|
||||
"cumulative_threshold": 30000,
|
||||
},
|
||||
)
|
||||
cat.save()
|
||||
|
||||
def test_tds_across_fiscal_year(self):
|
||||
"""
|
||||
Advance TDS on previous fiscal year should be properly allocated on Invoices in upcoming fiscal year
|
||||
--||-----FY 2023-----||-----FY 2024-----||--
|
||||
--||-----Advance-----||---Inv1---Inv2---||--
|
||||
"""
|
||||
self.set_previous_fy_and_tax_category()
|
||||
supplier = "Test TDS Supplier"
|
||||
# Cumulative threshold 30000 and tax rate 10%
|
||||
category = "Cumulative Threshold TDS"
|
||||
frappe.db.set_value(
|
||||
"Supplier",
|
||||
supplier,
|
||||
{
|
||||
"tax_withholding_category": category,
|
||||
"pan": "ABCTY1234D",
|
||||
},
|
||||
)
|
||||
po_and_advance_posting_date = add_days(self.prev_fy.year_end_date, -10)
|
||||
po = create_purchase_order(supplier=supplier, qty=10, rate=10000)
|
||||
po.transaction_date = po_and_advance_posting_date
|
||||
po.taxes = []
|
||||
po.apply_tds = False
|
||||
po.tax_withholding_category = None
|
||||
po.save().submit()
|
||||
|
||||
# Partial advance
|
||||
payment = get_payment_entry(po.doctype, po.name)
|
||||
payment.posting_date = po_and_advance_posting_date
|
||||
payment.paid_amount = 60000
|
||||
payment.apply_tax_withholding_amount = 1
|
||||
payment.tax_withholding_category = category
|
||||
payment.references = []
|
||||
payment.taxes = []
|
||||
payment.save().submit()
|
||||
|
||||
self.assertEqual(len(payment.taxes), 1)
|
||||
self.assertEqual(payment.taxes[0].tax_amount, 6000)
|
||||
|
||||
# Multiple partial invoices
|
||||
payment.reload()
|
||||
pi1 = make_purchase_invoice(source_name=po.name)
|
||||
pi1.apply_tds = True
|
||||
pi1.tax_withholding_category = category
|
||||
pi1.items[0].qty = 3
|
||||
pi1.items[0].rate = 10000
|
||||
advances = pi1.get_advance_entries()
|
||||
pi1.append(
|
||||
"advances",
|
||||
{
|
||||
"reference_type": advances[0].reference_type,
|
||||
"reference_name": advances[0].reference_name,
|
||||
"advance_amount": advances[0].amount,
|
||||
"allocated_amount": 30000,
|
||||
},
|
||||
)
|
||||
pi1.save().submit()
|
||||
pi1.reload()
|
||||
payment.reload()
|
||||
self.assertEqual(pi1.taxes, [])
|
||||
self.assertEqual(payment.taxes[0].tax_amount, 6000)
|
||||
self.assertEqual(payment.taxes[0].allocated_amount, 3000)
|
||||
|
||||
pi2 = make_purchase_invoice(source_name=po.name)
|
||||
pi2.apply_tds = True
|
||||
pi2.tax_withholding_category = category
|
||||
pi2.items[0].qty = 3
|
||||
pi2.items[0].rate = 10000
|
||||
advances = pi2.get_advance_entries()
|
||||
pi2.append(
|
||||
"advances",
|
||||
{
|
||||
"reference_type": advances[0].reference_type,
|
||||
"reference_name": advances[0].reference_name,
|
||||
"advance_amount": advances[0].amount,
|
||||
"allocated_amount": 30000,
|
||||
},
|
||||
)
|
||||
pi2.save().submit()
|
||||
pi2.reload()
|
||||
payment.reload()
|
||||
self.assertEqual(pi2.taxes, [])
|
||||
self.assertEqual(payment.taxes[0].tax_amount, 6000)
|
||||
self.assertEqual(payment.taxes[0].allocated_amount, 6000)
|
||||
|
||||
|
||||
def cancel_invoices():
|
||||
purchase_invoices = frappe.get_all(
|
||||
|
||||
@@ -238,10 +238,16 @@ def merge_similar_entries(gl_map, precision=None):
|
||||
same_head.debit_in_account_currency = flt(same_head.debit_in_account_currency) + flt(
|
||||
entry.debit_in_account_currency
|
||||
)
|
||||
same_head.debit_in_transaction_currency = flt(same_head.debit_in_transaction_currency) + flt(
|
||||
entry.debit_in_transaction_currency
|
||||
)
|
||||
same_head.credit = flt(same_head.credit) + flt(entry.credit)
|
||||
same_head.credit_in_account_currency = flt(same_head.credit_in_account_currency) + flt(
|
||||
entry.credit_in_account_currency
|
||||
)
|
||||
same_head.credit_in_transaction_currency = flt(same_head.credit_in_transaction_currency) + flt(
|
||||
entry.credit_in_transaction_currency
|
||||
)
|
||||
else:
|
||||
merged_gl_map.append(entry)
|
||||
|
||||
|
||||
@@ -751,52 +751,6 @@ def validate_party_frozen_disabled(party_type, party_name):
|
||||
frappe.msgprint(_("{0} {1} is not active").format(party_type, party_name), alert=True)
|
||||
|
||||
|
||||
def get_timeline_data(doctype, name):
|
||||
"""returns timeline data for the past one year"""
|
||||
from frappe.desk.form.load import get_communication_data
|
||||
|
||||
out = {}
|
||||
after = add_years(None, -1).strftime("%Y-%m-%d")
|
||||
|
||||
data = get_communication_data(
|
||||
doctype,
|
||||
name,
|
||||
after=after,
|
||||
group_by="group by communication_date",
|
||||
fields="C.communication_date as communication_date, count(C.name)",
|
||||
as_dict=False,
|
||||
)
|
||||
|
||||
# fetch and append data from Activity Log
|
||||
activity_log = frappe.qb.DocType("Activity Log")
|
||||
data += (
|
||||
frappe.qb.from_(activity_log)
|
||||
.select(activity_log.communication_date, Count(activity_log.name))
|
||||
.where(
|
||||
(
|
||||
((activity_log.reference_doctype == doctype) & (activity_log.reference_name == name))
|
||||
| ((activity_log.timeline_doctype == doctype) & (activity_log.timeline_name == name))
|
||||
| (
|
||||
(activity_log.reference_doctype.isin(["Quotation", "Opportunity"]))
|
||||
& (activity_log.timeline_name == name)
|
||||
)
|
||||
)
|
||||
& (activity_log.status != "Success")
|
||||
& (activity_log.creation > after)
|
||||
)
|
||||
.groupby(activity_log.communication_date)
|
||||
.orderby(activity_log.communication_date, order=frappe.qb.desc)
|
||||
).run()
|
||||
|
||||
timeline_items = dict(data)
|
||||
|
||||
for date, count in timeline_items.items():
|
||||
timestamp = get_timestamp(date)
|
||||
out.update({timestamp: count})
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def get_dashboard_info(party_type, party, loyalty_program=None):
|
||||
current_fiscal_year = get_fiscal_year(nowdate(), as_dict=True)
|
||||
|
||||
|
||||
@@ -501,8 +501,9 @@ class ReceivablePayableReport:
|
||||
# Deduct that from paid amount pre allocation
|
||||
row.paid -= flt(payment_terms_details[0].total_advance)
|
||||
|
||||
# If no or single payment terms, no need to split the row
|
||||
if len(payment_terms_details) <= 1:
|
||||
# If single payment terms, no need to split the row
|
||||
if len(payment_terms_details) == 1 and payment_terms_details[0].payment_term:
|
||||
self.append_payment_term(row, payment_terms_details[0], original_row)
|
||||
return
|
||||
|
||||
for d in payment_terms_details:
|
||||
|
||||
@@ -58,9 +58,9 @@ class Deferred_Item:
|
||||
For a given GL/Journal posting, get balance based on item type
|
||||
"""
|
||||
if self.type == "Deferred Sale Item":
|
||||
return entry.debit - entry.credit
|
||||
return flt(entry.debit) - flt(entry.credit)
|
||||
elif self.type == "Deferred Purchase Item":
|
||||
return -(entry.credit - entry.debit)
|
||||
return -(flt(entry.credit) - flt(entry.debit))
|
||||
return 0
|
||||
|
||||
def get_item_total(self):
|
||||
@@ -147,7 +147,7 @@ class Deferred_Item:
|
||||
actual = 0
|
||||
for posting in self.gle_entries:
|
||||
# if period.from_date <= posting.posting_date <= period.to_date:
|
||||
if period.from_date <= posting.gle_posting_date <= period.to_date:
|
||||
if period.from_date <= getdate(posting.gle_posting_date) <= period.to_date:
|
||||
period_sum += self.get_amount(posting)
|
||||
if posting.posted == "posted":
|
||||
actual += self.get_amount(posting)
|
||||
@@ -285,7 +285,7 @@ class Deferred_Revenue_and_Expense_Report:
|
||||
qb.from_(inv_item)
|
||||
.join(inv)
|
||||
.on(inv.name == inv_item.parent)
|
||||
.join(gle)
|
||||
.left_join(gle)
|
||||
.on((inv_item.name == gle.voucher_detail_no) & (deferred_account_field == gle.account))
|
||||
.select(
|
||||
inv.name.as_("doc"),
|
||||
|
||||
@@ -279,3 +279,79 @@ class TestDeferredRevenueAndExpense(FrappeTestCase, AccountsTestMixin):
|
||||
{"key": "aug_2021", "total": 0, "actual": 0},
|
||||
]
|
||||
self.assertEqual(report.period_total, expected)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"book_deferred_entries_based_on": "Months", "book_deferred_entries_via_journal_entry": 0},
|
||||
)
|
||||
def test_zero_amount(self):
|
||||
self.create_item("_Test Office Desk", 0, self.warehouse, self.company)
|
||||
item = frappe.get_doc("Item", self.item)
|
||||
item.enable_deferred_expense = 1
|
||||
item.item_defaults[0].deferred_expense_account = self.deferred_expense_account
|
||||
item.no_of_months_exp = 12
|
||||
item.save()
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
supplier=self.supplier,
|
||||
is_return=False,
|
||||
update_stock=False,
|
||||
posting_date=frappe.utils.datetime.date(2021, 12, 30),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
do_not_save=True,
|
||||
rate=3910,
|
||||
price_list_rate=3910,
|
||||
warehouse=self.warehouse,
|
||||
qty=1,
|
||||
)
|
||||
pi.set_posting_time = True
|
||||
pi.items[0].enable_deferred_expense = 1
|
||||
pi.items[0].service_start_date = "2021-12-30"
|
||||
pi.items[0].service_end_date = "2022-12-30"
|
||||
pi.items[0].deferred_expense_account = self.deferred_expense_account
|
||||
pi.items[0].expense_account = self.expense_account
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
pda = frappe.get_doc(
|
||||
doctype="Process Deferred Accounting",
|
||||
posting_date=nowdate(),
|
||||
start_date="2022-01-01",
|
||||
end_date="2022-01-31",
|
||||
type="Expense",
|
||||
company=self.company,
|
||||
)
|
||||
pda.insert()
|
||||
pda.submit()
|
||||
|
||||
# execute report
|
||||
fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2022-01-31"))
|
||||
self.filters = frappe._dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"filter_based_on": "Date Range",
|
||||
"period_start_date": "2022-01-01",
|
||||
"period_end_date": "2022-01-31",
|
||||
"from_fiscal_year": fiscal_year.year,
|
||||
"to_fiscal_year": fiscal_year.year,
|
||||
"periodicity": "Monthly",
|
||||
"type": "Expense",
|
||||
"with_upcoming_postings": False,
|
||||
}
|
||||
)
|
||||
|
||||
report = Deferred_Revenue_and_Expense_Report(filters=self.filters)
|
||||
report.run()
|
||||
|
||||
# fetch the invoice from deferred invoices list
|
||||
inv = [d for d in report.deferred_invoices if d.name == pi.name]
|
||||
# make sure the list isn't empty
|
||||
self.assertTrue(inv)
|
||||
# calculate the total deferred expense for the period
|
||||
inv = inv[0].calculate_invoice_revenue_expense_for_period()
|
||||
deferred_exp = sum([inv[idx].actual for idx in range(len(report.period_list))])
|
||||
# make sure the total deferred expense is greater than 0
|
||||
self.assertLess(deferred_exp, 0)
|
||||
|
||||
@@ -720,20 +720,22 @@ class GrossProfitGenerator:
|
||||
frappe.qb.from_(purchase_invoice_item)
|
||||
.inner_join(purchase_invoice)
|
||||
.on(purchase_invoice.name == purchase_invoice_item.parent)
|
||||
.select(purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor)
|
||||
.select(
|
||||
purchase_invoice.name,
|
||||
purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor,
|
||||
)
|
||||
.where(purchase_invoice.docstatus == 1)
|
||||
.where(purchase_invoice.posting_date <= self.filters.to_date)
|
||||
.where(purchase_invoice_item.item_code == item_code)
|
||||
)
|
||||
|
||||
if row.project:
|
||||
query.where(purchase_invoice_item.project == row.project)
|
||||
query = query.where(purchase_invoice_item.project == row.project)
|
||||
|
||||
if row.cost_center:
|
||||
query.where(purchase_invoice_item.cost_center == row.cost_center)
|
||||
query = query.where(purchase_invoice_item.cost_center == row.cost_center)
|
||||
|
||||
query.orderby(purchase_invoice.posting_date, order=frappe.qb.desc)
|
||||
query.limit(1)
|
||||
query = query.orderby(purchase_invoice.posting_date, order=frappe.qb.desc).limit(1)
|
||||
last_purchase_rate = query.run()
|
||||
|
||||
return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0
|
||||
|
||||
@@ -516,6 +516,10 @@ def reconcile_against_document(
|
||||
doc.make_advance_gl_entries()
|
||||
else:
|
||||
gl_map = doc.build_gl_map()
|
||||
# Make sure there is no overallocation
|
||||
from erpnext.accounts.general_ledger import process_debit_credit_difference
|
||||
|
||||
process_debit_credit_difference(gl_map)
|
||||
create_payment_ledger_entry(gl_map, update_outstanding="No", cancel=0, adv_adj=1)
|
||||
|
||||
# Only update outstanding for newly linked vouchers
|
||||
@@ -1094,7 +1098,7 @@ def get_companies():
|
||||
def get_children(doctype, parent, company, is_root=False):
|
||||
from erpnext.accounts.report.financial_statements import sort_accounts
|
||||
|
||||
parent_fieldname = "parent_" + doctype.lower().replace(" ", "_")
|
||||
parent_fieldname = "parent_" + frappe.scrub(doctype)
|
||||
fields = ["name as value", "is_group as expandable"]
|
||||
filters = [["docstatus", "<", 2]]
|
||||
|
||||
|
||||
@@ -143,6 +143,10 @@ class AssetCapitalization(StockController):
|
||||
self.make_gl_entries()
|
||||
self.restore_consumed_asset_items()
|
||||
|
||||
def on_trash(self):
|
||||
frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None)
|
||||
super(AssetCapitalization, self).on_trash()
|
||||
|
||||
def cancel_target_asset(self):
|
||||
if self.entry_type == "Capitalization" and self.target_asset:
|
||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||
|
||||
@@ -305,6 +305,7 @@ def create_asset_repair(**args):
|
||||
"serial_nos": args.serial_no,
|
||||
"posting_date": today(),
|
||||
"posting_time": nowtime(),
|
||||
"do_not_submit": 1,
|
||||
}
|
||||
)
|
||||
).name
|
||||
|
||||
@@ -612,6 +612,20 @@ class PurchaseOrder(BuyingController):
|
||||
|
||||
return result
|
||||
|
||||
def update_ordered_qty_in_so_for_removed_items(self, removed_items):
|
||||
"""
|
||||
Updates ordered_qty in linked SO when item rows are removed using Update Items
|
||||
"""
|
||||
if not self.is_against_so():
|
||||
return
|
||||
for item in removed_items:
|
||||
prev_ordered_qty = frappe.get_cached_value(
|
||||
"Sales Order Item", item.get("sales_order_item"), "ordered_qty"
|
||||
)
|
||||
frappe.db.set_value(
|
||||
"Sales Order Item", item.get("sales_order_item"), "ordered_qty", prev_ordered_qty - item.qty
|
||||
)
|
||||
|
||||
def auto_create_subcontracting_order(self):
|
||||
if self.is_subcontracted and not self.is_old_subcontracting_flow:
|
||||
if frappe.db.get_single_value("Buying Settings", "auto_create_subcontracting_order"):
|
||||
|
||||
@@ -764,12 +764,7 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
}
|
||||
).insert()
|
||||
else:
|
||||
account = frappe.db.get_value(
|
||||
"Account",
|
||||
filters={"account_name": account_name, "company": company},
|
||||
fieldname="name",
|
||||
pluck=True,
|
||||
)
|
||||
account = frappe.get_doc("Account", {"account_name": account_name, "company": company})
|
||||
|
||||
return account
|
||||
|
||||
@@ -800,22 +795,6 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
|
||||
|
||||
pi = make_purchase_invoice(po_doc.name)
|
||||
pi.append(
|
||||
"advances",
|
||||
{
|
||||
"reference_type": pe.doctype,
|
||||
"reference_name": pe.name,
|
||||
"reference_row": pe.references[0].name,
|
||||
"advance_amount": 5000,
|
||||
"allocated_amount": 5000,
|
||||
},
|
||||
)
|
||||
pi.save().submit()
|
||||
pe.reload()
|
||||
po_doc.reload()
|
||||
self.assertEqual(po_doc.advance_paid, 0)
|
||||
|
||||
company_doc.book_advance_payments_in_separate_party_account = False
|
||||
company_doc.save()
|
||||
|
||||
|
||||
@@ -3,10 +3,6 @@ from frappe import _
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
"heatmap": True,
|
||||
"heatmap_message": _(
|
||||
"This is based on transactions against this Supplier. See timeline below for details"
|
||||
),
|
||||
"fieldname": "supplier",
|
||||
"non_standard_fieldnames": {"Payment Entry": "party", "Bank Account": "party"},
|
||||
"transactions": [
|
||||
|
||||
@@ -133,6 +133,13 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
|
||||
return row.supplier_name;
|
||||
});
|
||||
|
||||
let items = [];
|
||||
report.data.forEach((d) => {
|
||||
if (!items.includes(d.item_code)) {
|
||||
items.push(d.item_code);
|
||||
}
|
||||
});
|
||||
|
||||
// Create a dialog window for the user to pick their supplier
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __("Select Default Supplier"),
|
||||
@@ -151,20 +158,34 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
reqd: 1,
|
||||
label: "Item",
|
||||
fieldtype: "Link",
|
||||
options: "Item",
|
||||
fieldname: "item_code",
|
||||
get_query: () => {
|
||||
return {
|
||||
filters: {
|
||||
name: ["in", items],
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
dialog.set_primary_action(__("Set Default Supplier"), () => {
|
||||
let values = dialog.get_values();
|
||||
|
||||
if (values) {
|
||||
// Set the default_supplier field of the appropriate Item to the selected supplier
|
||||
frappe.call({
|
||||
method: "frappe.client.set_value",
|
||||
method: "erpnext.buying.report.supplier_quotation_comparison.supplier_quotation_comparison.set_default_supplier",
|
||||
args: {
|
||||
doctype: "Item",
|
||||
name: item_code,
|
||||
fieldname: "default_supplier",
|
||||
value: values.supplier,
|
||||
item_code: values.item_code,
|
||||
supplier: values.supplier,
|
||||
company: filters.company,
|
||||
},
|
||||
freeze: true,
|
||||
callback: (r) => {
|
||||
|
||||
@@ -292,3 +292,13 @@ def get_message():
|
||||
<span class="indicator red">
|
||||
Expires today / Already Expired
|
||||
</span>"""
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_default_supplier(item_code, supplier, company):
|
||||
frappe.db.set_value(
|
||||
"Item Default",
|
||||
{"parent": item_code, "company": company},
|
||||
"default_supplier",
|
||||
supplier,
|
||||
)
|
||||
|
||||
@@ -3158,6 +3158,9 @@ def validate_and_delete_children(parent, data) -> bool:
|
||||
d.cancel()
|
||||
d.delete()
|
||||
|
||||
if parent.doctype == "Purchase Order":
|
||||
parent.update_ordered_qty_in_so_for_removed_items(deleted_children)
|
||||
|
||||
# need to update ordered qty in Material Request first
|
||||
# bin uses Material Request Items to recalculate & update
|
||||
parent.update_prevdoc_status()
|
||||
|
||||
@@ -1227,8 +1227,8 @@ def get_accounting_ledger_preview(doc, filters):
|
||||
"debit",
|
||||
"credit",
|
||||
"against",
|
||||
"party",
|
||||
"party_type",
|
||||
"party",
|
||||
"cost_center",
|
||||
"against_voucher_type",
|
||||
"against_voucher",
|
||||
@@ -1404,7 +1404,12 @@ def is_reposting_pending():
|
||||
)
|
||||
|
||||
|
||||
def future_sle_exists(args, sl_entries=None):
|
||||
def future_sle_exists(args, sl_entries=None, allow_force_reposting=True):
|
||||
if allow_force_reposting and frappe.db.get_single_value(
|
||||
"Stock Reposting Settings", "do_reposting_for_each_stock_transaction"
|
||||
):
|
||||
return True
|
||||
|
||||
key = (args.voucher_type, args.voucher_no)
|
||||
if not hasattr(frappe.local, "future_sle"):
|
||||
frappe.local.future_sle = {}
|
||||
|
||||
@@ -121,7 +121,7 @@ def send_mail(entry, email_campaign):
|
||||
doctype="Email Campaign",
|
||||
name=email_campaign.name,
|
||||
subject=frappe.render_template(email_template.get("subject"), context),
|
||||
content=frappe.render_template(email_template.get("response"), context),
|
||||
content=frappe.render_template(email_template.response_, context),
|
||||
sender=sender,
|
||||
recipients=recipient_list,
|
||||
communication_medium="Email",
|
||||
|
||||
@@ -1896,7 +1896,7 @@ def sales_order_query(doctype=None, txt=None, searchfield=None, start=None, page
|
||||
query = query.where(so_table.name.isin(filters.get("sales_orders")))
|
||||
|
||||
if txt:
|
||||
query = query.where(table.item_code.like(f"{txt}%"))
|
||||
query = query.where(table.parent.like(f"%{txt}%"))
|
||||
|
||||
if page_len:
|
||||
query = query.limit(page_len)
|
||||
|
||||
@@ -948,6 +948,21 @@ class WorkOrder(Document):
|
||||
if not self.qty > 0:
|
||||
frappe.throw(_("Quantity to Manufacture must be greater than 0."))
|
||||
|
||||
if (
|
||||
self.stock_uom
|
||||
and frappe.get_cached_value("UOM", self.stock_uom, "must_be_whole_number")
|
||||
and abs(cint(self.qty) - flt(self.qty, self.precision("qty"))) > 0.0000001
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Qty To Manufacture ({0}) cannot be a fraction for the UOM {2}. To allow this, disable '{1}' in the UOM {2}."
|
||||
).format(
|
||||
flt(self.qty, self.precision("qty")),
|
||||
frappe.bold(_("Must be Whole Number")),
|
||||
frappe.bold(self.stock_uom),
|
||||
),
|
||||
)
|
||||
|
||||
if self.production_plan and self.production_plan_item and not self.production_plan_sub_assembly_item:
|
||||
qty_dict = frappe.db.get_value(
|
||||
"Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1
|
||||
|
||||
@@ -360,4 +360,6 @@ erpnext.patches.v15_0.create_accounting_dimensions_in_payment_request
|
||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
|
||||
erpnext.patches.v14_0.set_maintain_stock_for_bom_item
|
||||
erpnext.patches.v15_0.delete_orphaned_asset_movement_item_records
|
||||
erpnext.patches.v15_0.delete_orphaned_asset_movement_item_records
|
||||
erpnext.patches.v15_0.fix_debit_credit_in_transaction_currency
|
||||
erpnext.patches.v15_0.remove_cancelled_asset_capitalization_from_asset
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
# update debit and credit in transaction currency:
|
||||
# if transaction currency is same as account currency,
|
||||
# then debit and credit in transaction currency is same as debit and credit in account currency
|
||||
# else debit and credit divided by exchange rate
|
||||
|
||||
# nosemgrep
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE `tabGL Entry`
|
||||
SET
|
||||
debit_in_transaction_currency = IF(transaction_currency = account_currency, debit_in_account_currency, debit / transaction_exchange_rate),
|
||||
credit_in_transaction_currency = IF(transaction_currency = account_currency, credit_in_account_currency, credit / transaction_exchange_rate)
|
||||
WHERE
|
||||
transaction_exchange_rate > 0
|
||||
and transaction_currency is not null
|
||||
"""
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
cancelled_asset_capitalizations = frappe.get_all(
|
||||
"Asset Capitalization",
|
||||
filters={"docstatus": 2},
|
||||
fields=["name", "target_asset"],
|
||||
)
|
||||
for asset_capitalization in cancelled_asset_capitalizations:
|
||||
frappe.db.set_value("Asset", asset_capitalization.target_asset, "capitalized_in", None)
|
||||
@@ -454,7 +454,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"max_attachments": 4,
|
||||
"modified": "2024-01-08 16:01:34.598258",
|
||||
"modified": "2024-04-24 10:56:16.001032",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Projects",
|
||||
"name": "Project",
|
||||
@@ -489,6 +489,15 @@
|
||||
"role": "Projects Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"report": 1,
|
||||
"role": "Employee",
|
||||
"select": 1,
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
|
||||
@@ -51,38 +51,7 @@ $.extend(erpnext, {
|
||||
},
|
||||
|
||||
setup_serial_or_batch_no: function () {
|
||||
let grid_row = cur_frm.open_grid_row();
|
||||
if (
|
||||
!grid_row ||
|
||||
!grid_row.grid_form.fields_dict.serial_no ||
|
||||
grid_row.grid_form.fields_dict.serial_no.get_status() !== "Write"
|
||||
)
|
||||
return;
|
||||
|
||||
frappe.model.get_value(
|
||||
"Item",
|
||||
{ name: grid_row.doc.item_code },
|
||||
["has_serial_no", "has_batch_no"],
|
||||
({ has_serial_no, has_batch_no }) => {
|
||||
Object.assign(grid_row.doc, { has_serial_no, has_batch_no });
|
||||
|
||||
if (has_serial_no) {
|
||||
attach_selector_button(
|
||||
__("Add Serial No"),
|
||||
grid_row.grid_form.fields_dict.serial_no.$wrapper,
|
||||
this,
|
||||
grid_row
|
||||
);
|
||||
} else if (has_batch_no) {
|
||||
attach_selector_button(
|
||||
__("Pick Batch No"),
|
||||
grid_row.grid_form.fields_dict.batch_no.$wrapper,
|
||||
this,
|
||||
grid_row
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
// Deprecated in v15
|
||||
},
|
||||
|
||||
route_to_adjustment_jv: (args) => {
|
||||
@@ -937,11 +906,14 @@ erpnext.utils.map_current_doc = function (opts) {
|
||||
if (opts.source_doctype) {
|
||||
let data_fields = [];
|
||||
if (["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype)) {
|
||||
data_fields.push({
|
||||
fieldname: "merge_taxes",
|
||||
fieldtype: "Check",
|
||||
label: __("Merge taxes from multiple documents"),
|
||||
});
|
||||
let target_meta = frappe.get_meta(cur_frm.doc.doctype);
|
||||
if (target_meta.fields.find((f) => f.fieldname === "taxes")) {
|
||||
data_fields.push({
|
||||
fieldname: "merge_taxes",
|
||||
fieldtype: "Check",
|
||||
label: __("Merge taxes from multiple documents"),
|
||||
});
|
||||
}
|
||||
}
|
||||
const d = new frappe.ui.form.MultiSelectDialog({
|
||||
doctype: opts.source_doctype,
|
||||
|
||||
@@ -373,6 +373,7 @@ erpnext.sales_common = {
|
||||
frappe.model.set_value(item.doctype, item.name, {
|
||||
serial_and_batch_bundle: r.name,
|
||||
use_serial_batch_fields: 0,
|
||||
incoming_rate: r.avg_rate,
|
||||
qty:
|
||||
qty /
|
||||
flt(
|
||||
|
||||
@@ -135,14 +135,51 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-18 08:25:35.302081",
|
||||
"modified": "2024-04-18 15:25:25.808355",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Regional",
|
||||
"name": "Lower Deduction Certificate",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
|
||||
@@ -3,10 +3,6 @@ from frappe import _
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
"heatmap": True,
|
||||
"heatmap_message": _(
|
||||
"This is based on transactions against this Customer. See timeline below for details"
|
||||
),
|
||||
"fieldname": "customer",
|
||||
"non_standard_fieldnames": {
|
||||
"Payment Entry": "party",
|
||||
|
||||
@@ -707,6 +707,8 @@
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.book_advance_payments_in_separate_party_account",
|
||||
"description": "Only 'Payment Entries' made against this advance account are supported.",
|
||||
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account",
|
||||
"fieldname": "default_advance_received_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Advance Received Account",
|
||||
@@ -715,6 +717,8 @@
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.book_advance_payments_in_separate_party_account",
|
||||
"description": "Only 'Payment Entries' made against this advance account are supported.",
|
||||
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account",
|
||||
"fieldname": "default_advance_paid_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Advance Paid Account",
|
||||
@@ -782,7 +786,7 @@
|
||||
"image_field": "company_logo",
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-10 21:53:13.860791",
|
||||
"modified": "2024-04-23 12:38:33.173938",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Company",
|
||||
|
||||
@@ -35,7 +35,7 @@ class Company(NestedSet):
|
||||
auto_exchange_rate_revaluation: DF.Check
|
||||
book_advance_payments_in_separate_party_account: DF.Check
|
||||
capital_work_in_progress_account: DF.Link | None
|
||||
chart_of_accounts: DF.Literal
|
||||
chart_of_accounts: DF.Literal[None]
|
||||
company_description: DF.TextEditor | None
|
||||
company_logo: DF.AttachImage | None
|
||||
company_name: DF.Data
|
||||
@@ -139,6 +139,7 @@ class Company(NestedSet):
|
||||
self.validate_abbr()
|
||||
self.validate_default_accounts()
|
||||
self.validate_currency()
|
||||
self.validate_advance_account_currency()
|
||||
self.validate_coa_input()
|
||||
self.validate_perpetual_inventory()
|
||||
self.validate_provisional_account_for_non_stock_items()
|
||||
@@ -192,6 +193,29 @@ class Company(NestedSet):
|
||||
).format(frappe.bold(account[0]))
|
||||
frappe.throw(error_message)
|
||||
|
||||
def validate_advance_account_currency(self):
|
||||
if (
|
||||
self.default_advance_received_account
|
||||
and frappe.get_cached_value("Account", self.default_advance_received_account, "account_currency")
|
||||
!= self.default_currency
|
||||
):
|
||||
frappe.throw(
|
||||
_("'{0}' should be in company currency {1}.").format(
|
||||
frappe.bold("Default Advance Received Account"), frappe.bold(self.default_currency)
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
self.default_advance_paid_account
|
||||
and frappe.get_cached_value("Account", self.default_advance_paid_account, "account_currency")
|
||||
!= self.default_currency
|
||||
):
|
||||
frappe.throw(
|
||||
_("'{0}' should be in company currency {1}.").format(
|
||||
frappe.bold("Default Advance Paid Account"), frappe.bold(self.default_currency)
|
||||
)
|
||||
)
|
||||
|
||||
def validate_currency(self):
|
||||
if self.is_new():
|
||||
return
|
||||
|
||||
@@ -18,18 +18,6 @@ erpnext.setup.EmployeeController = class EmployeeController extends frappe.ui.fo
|
||||
refresh() {
|
||||
erpnext.toggle_naming_series();
|
||||
}
|
||||
|
||||
salutation() {
|
||||
if (this.frm.doc.salutation) {
|
||||
this.frm.set_value(
|
||||
"gender",
|
||||
{
|
||||
Mr: "Male",
|
||||
Ms: "Female",
|
||||
}[this.frm.doc.salutation]
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
frappe.ui.form.on("Employee", {
|
||||
|
||||
@@ -86,6 +86,7 @@ class TestBatch(FrappeTestCase):
|
||||
"batches": frappe._dict({batch_no: 20}),
|
||||
"type_of_transaction": "Inward",
|
||||
"company": receipt.company,
|
||||
"do_not_submit": 1,
|
||||
}
|
||||
)
|
||||
.make_serial_and_batch_bundle()
|
||||
@@ -176,6 +177,7 @@ class TestBatch(FrappeTestCase):
|
||||
"batches": frappe._dict({batch_no: batch_qty}),
|
||||
"type_of_transaction": "Outward",
|
||||
"company": receipt.company,
|
||||
"do_not_submit": 1,
|
||||
}
|
||||
)
|
||||
.make_serial_and_batch_bundle()
|
||||
@@ -249,6 +251,7 @@ class TestBatch(FrappeTestCase):
|
||||
"batches": frappe._dict({batch_no: batch_qty}),
|
||||
"type_of_transaction": "Outward",
|
||||
"company": receipt.company,
|
||||
"do_not_submit": 1,
|
||||
}
|
||||
)
|
||||
.make_serial_and_batch_bundle()
|
||||
@@ -341,6 +344,7 @@ class TestBatch(FrappeTestCase):
|
||||
"batches": frappe._dict({batch_name: 90}),
|
||||
"type_of_transaction": "Inward",
|
||||
"company": "_Test Company",
|
||||
"do_not_submit": 1,
|
||||
}
|
||||
).make_serial_and_batch_bundle()
|
||||
|
||||
|
||||
@@ -238,7 +238,7 @@ def update_qty(bin_name, args):
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
|
||||
# actual qty is not up to date in case of backdated transaction
|
||||
if future_sle_exists(args):
|
||||
if future_sle_exists(args, allow_force_reposting=False):
|
||||
last_sle_qty = (
|
||||
frappe.qb.from_(sle)
|
||||
.select(sle.qty_after_transaction)
|
||||
|
||||
@@ -1066,7 +1066,7 @@ def make_sales_invoice(source_name, target_doc=None, args=None):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_delivery_trip(source_name, target_doc=None):
|
||||
def make_delivery_trip(source_name, target_doc=None, kwargs=None):
|
||||
def update_stop_details(source_doc, target_doc, source_parent):
|
||||
target_doc.customer = source_parent.customer
|
||||
target_doc.address = source_parent.shipping_address_name
|
||||
@@ -1099,7 +1099,7 @@ def make_delivery_trip(source_name, target_doc=None):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_installation_note(source_name, target_doc=None):
|
||||
def make_installation_note(source_name, target_doc=None, kwargs=None):
|
||||
def update_item(obj, target, source_parent):
|
||||
target.qty = flt(obj.qty) - flt(obj.installed_qty)
|
||||
target.serial_no = obj.serial_no
|
||||
|
||||
@@ -51,6 +51,7 @@ frappe.ui.form.on("Delivery Trip", {
|
||||
frm.add_custom_button(
|
||||
__("Delivery Note"),
|
||||
() => {
|
||||
frm.clear_table("delivery_stops");
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_delivery_trip",
|
||||
source_doctype: "Delivery Note",
|
||||
|
||||
@@ -15,6 +15,9 @@ frappe.ui.form.on("Item", {
|
||||
frm.add_fetch("tax_type", "tax_rate", "tax_rate");
|
||||
|
||||
frm.make_methods = {
|
||||
Quotation: () => {
|
||||
open_form(frm, "Quotation", "Quotation Item", "items");
|
||||
},
|
||||
"Sales Order": () => {
|
||||
open_form(frm, "Sales Order", "Sales Order Item", "items");
|
||||
},
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
"section_break_11",
|
||||
"description",
|
||||
"brand",
|
||||
"unit_of_measure_conversion",
|
||||
"uoms",
|
||||
"dashboard_tab",
|
||||
"inventory_section",
|
||||
"inventory_settings_section",
|
||||
@@ -52,8 +54,6 @@
|
||||
"barcodes",
|
||||
"reorder_section",
|
||||
"reorder_levels",
|
||||
"unit_of_measure_conversion",
|
||||
"uoms",
|
||||
"serial_nos_and_batches",
|
||||
"has_batch_no",
|
||||
"create_new_batch",
|
||||
@@ -891,7 +891,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2024-01-08 18:09:30.225085",
|
||||
"modified": "2024-04-30 13:46:39.098753",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item",
|
||||
@@ -964,4 +964,4 @@
|
||||
"states": [],
|
||||
"title_field": "item_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,15 +65,13 @@ class Item(Document):
|
||||
from erpnext.stock.doctype.item_reorder.item_reorder import ItemReorder
|
||||
from erpnext.stock.doctype.item_supplier.item_supplier import ItemSupplier
|
||||
from erpnext.stock.doctype.item_tax.item_tax import ItemTax
|
||||
from erpnext.stock.doctype.item_variant_attribute.item_variant_attribute import (
|
||||
ItemVariantAttribute,
|
||||
)
|
||||
from erpnext.stock.doctype.item_variant_attribute.item_variant_attribute import ItemVariantAttribute
|
||||
from erpnext.stock.doctype.uom_conversion_detail.uom_conversion_detail import UOMConversionDetail
|
||||
|
||||
allow_alternative_item: DF.Check
|
||||
allow_negative_stock: DF.Check
|
||||
asset_category: DF.Link | None
|
||||
asset_naming_series: DF.Literal
|
||||
asset_naming_series: DF.Literal[None]
|
||||
attributes: DF.Table[ItemVariantAttribute]
|
||||
auto_create_assets: DF.Check
|
||||
barcodes: DF.Table[ItemBarcode]
|
||||
|
||||
@@ -52,10 +52,13 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.stock_uom",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "UOM",
|
||||
"options": "UOM"
|
||||
"options": "UOM",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -220,7 +223,7 @@
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-01-30 14:02:19.304854",
|
||||
"modified": "2024-04-02 22:18:00.450641",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item Price",
|
||||
|
||||
@@ -38,7 +38,7 @@ class ItemPrice(Document):
|
||||
reference: DF.Data | None
|
||||
selling: DF.Check
|
||||
supplier: DF.Link | None
|
||||
uom: DF.Link | None
|
||||
uom: DF.Link
|
||||
valid_from: DF.Date | None
|
||||
valid_upto: DF.Date | None
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -790,7 +790,7 @@ def get_available_item_locations(
|
||||
locations = get_locations_based_on_required_qty(locations, required_qty)
|
||||
|
||||
if not ignore_validation:
|
||||
validate_picked_materials(item_code, required_qty, locations)
|
||||
validate_picked_materials(item_code, required_qty, locations, picked_item_details)
|
||||
|
||||
return locations
|
||||
|
||||
@@ -810,7 +810,7 @@ def get_locations_based_on_required_qty(locations, required_qty):
|
||||
return filtered_locations
|
||||
|
||||
|
||||
def validate_picked_materials(item_code, required_qty, locations):
|
||||
def validate_picked_materials(item_code, required_qty, locations, picked_item_details=None):
|
||||
for location in list(locations):
|
||||
if location["qty"] < 0:
|
||||
locations.remove(location)
|
||||
@@ -819,15 +819,25 @@ def validate_picked_materials(item_code, required_qty, locations):
|
||||
remaining_qty = required_qty - total_qty_available
|
||||
|
||||
if remaining_qty > 0:
|
||||
frappe.msgprint(
|
||||
_("{0} units of Item {1} is picked in another Pick List.").format(
|
||||
remaining_qty, get_link_to_form("Item", item_code)
|
||||
),
|
||||
title=_("Already Picked"),
|
||||
)
|
||||
if picked_item_details:
|
||||
frappe.msgprint(
|
||||
_("{0} units of Item {1} is picked in another Pick List.").format(
|
||||
remaining_qty, get_link_to_form("Item", item_code)
|
||||
),
|
||||
title=_("Already Picked"),
|
||||
)
|
||||
|
||||
else:
|
||||
frappe.msgprint(
|
||||
_("{0} units of Item {1} is not available in any of the warehouses.").format(
|
||||
remaining_qty, get_link_to_form("Item", item_code)
|
||||
),
|
||||
title=_("Insufficient Stock"),
|
||||
)
|
||||
|
||||
|
||||
def filter_locations_by_picked_materials(locations, picked_item_details) -> list[dict]:
|
||||
filterd_locations = []
|
||||
for row in locations:
|
||||
key = row.warehouse
|
||||
if row.batch_no:
|
||||
@@ -835,6 +845,7 @@ def filter_locations_by_picked_materials(locations, picked_item_details) -> list
|
||||
|
||||
picked_qty = picked_item_details.get(key, {}).get("picked_qty", 0)
|
||||
if not picked_qty:
|
||||
filterd_locations.append(row)
|
||||
continue
|
||||
if picked_qty > row.qty:
|
||||
row.qty = 0
|
||||
@@ -845,7 +856,10 @@ def filter_locations_by_picked_materials(locations, picked_item_details) -> list
|
||||
if row.serial_nos:
|
||||
row.serial_nos = list(set(row.serial_nos) - set(picked_item_details[key].get("serial_no")))
|
||||
|
||||
return locations
|
||||
if row.qty > 0:
|
||||
filterd_locations.append(row)
|
||||
|
||||
return filterd_locations
|
||||
|
||||
|
||||
def get_available_item_locations_for_serial_and_batched_item(
|
||||
|
||||
@@ -977,3 +977,157 @@ class TestPickList(FrappeTestCase):
|
||||
so = make_sales_order(item_code=item, qty=4, rate=100)
|
||||
pl = create_pick_list(so.name)
|
||||
self.assertFalse(hasattr(pl, "locations"))
|
||||
|
||||
def test_pick_list_validation_for_multiple_batches_and_sales_order(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item = make_item(
|
||||
"Test Batch Pick List Item For Multiple Batches",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"batch_number_series": "SN-BT-BATCH-SPLIMBATCH-.####",
|
||||
"create_new_batch": 1,
|
||||
},
|
||||
).name
|
||||
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=5)
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=5)
|
||||
|
||||
so = make_sales_order(item_code=item, qty=6, rate=100)
|
||||
|
||||
pl1 = create_pick_list(so.name)
|
||||
pl1.save()
|
||||
self.assertEqual(pl1.locations[0].qty, 5.0)
|
||||
self.assertEqual(pl1.locations[1].qty, 1.0)
|
||||
|
||||
so = make_sales_order(item_code=item, qty=4, rate=100)
|
||||
|
||||
pl = create_pick_list(so.name)
|
||||
pl.save()
|
||||
self.assertEqual(pl.locations[0].qty, 4.0)
|
||||
self.assertTrue(hasattr(pl, "locations"))
|
||||
|
||||
pl1.submit()
|
||||
|
||||
pl.reload()
|
||||
pl.submit()
|
||||
self.assertEqual(pl.locations[0].qty, 4.0)
|
||||
self.assertTrue(hasattr(pl, "locations"))
|
||||
|
||||
def test_pick_list_for_multiple_sales_order_with_multiple_batches(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item = make_item(
|
||||
"Test Batch Pick List Item For Multiple Batches and Sales Order",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"batch_number_series": "SN-SOO-BT-SPLIMBATCH-.####",
|
||||
"create_new_batch": 1,
|
||||
},
|
||||
).name
|
||||
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
|
||||
|
||||
so = make_sales_order(item_code=item, qty=10, rate=100)
|
||||
|
||||
pl1 = create_pick_list(so.name)
|
||||
pl1.save()
|
||||
self.assertEqual(pl1.locations[0].qty, 10)
|
||||
|
||||
so = make_sales_order(item_code=item, qty=110, rate=100)
|
||||
|
||||
pl = create_pick_list(so.name)
|
||||
pl.save()
|
||||
self.assertEqual(pl.locations[0].qty, 90.0)
|
||||
self.assertEqual(pl.locations[1].qty, 20.0)
|
||||
self.assertTrue(hasattr(pl, "locations"))
|
||||
|
||||
pl1.submit()
|
||||
|
||||
pl.reload()
|
||||
pl.submit()
|
||||
self.assertEqual(pl.locations[0].qty, 90.0)
|
||||
self.assertEqual(pl.locations[1].qty, 20.0)
|
||||
self.assertTrue(hasattr(pl, "locations"))
|
||||
|
||||
def test_pick_list_for_multiple_sales_order_with_multiple_serial_nos(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item = make_item(
|
||||
"Test Serial No Pick List Item For Multiple Batches and Sales Order",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "SNNN-SOO-BT-SPLIMBATCH-.####",
|
||||
},
|
||||
).name
|
||||
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
|
||||
|
||||
so = make_sales_order(item_code=item, qty=10, rate=100)
|
||||
|
||||
pl1 = create_pick_list(so.name)
|
||||
pl1.save()
|
||||
self.assertEqual(pl1.locations[0].qty, 10)
|
||||
|
||||
serial_nos = pl1.locations[0].serial_no.split("\n")
|
||||
self.assertEqual(len(serial_nos), 10)
|
||||
|
||||
so = make_sales_order(item_code=item, qty=110, rate=100)
|
||||
|
||||
pl = create_pick_list(so.name)
|
||||
pl.save()
|
||||
self.assertEqual(pl.locations[0].qty, 110.0)
|
||||
self.assertTrue(hasattr(pl, "locations"))
|
||||
|
||||
new_serial_nos = pl.locations[0].serial_no.split("\n")
|
||||
self.assertEqual(len(new_serial_nos), 110)
|
||||
|
||||
for sn in serial_nos:
|
||||
self.assertFalse(sn in new_serial_nos)
|
||||
|
||||
pl1.submit()
|
||||
|
||||
pl.reload()
|
||||
pl.submit()
|
||||
self.assertEqual(pl.locations[0].qty, 110.0)
|
||||
self.assertTrue(hasattr(pl, "locations"))
|
||||
|
||||
def test_pick_list_for_multiple_sales_orders_for_non_serialized_item(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item = make_item(
|
||||
"Test Non Serialized Pick List Item For Multiple Batches and Sales Order",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
).name
|
||||
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
|
||||
|
||||
so = make_sales_order(item_code=item, qty=10, rate=100)
|
||||
|
||||
pl1 = create_pick_list(so.name)
|
||||
pl1.save()
|
||||
self.assertEqual(pl1.locations[0].qty, 10)
|
||||
|
||||
so = make_sales_order(item_code=item, qty=110, rate=100)
|
||||
|
||||
pl = create_pick_list(so.name)
|
||||
pl.save()
|
||||
self.assertEqual(pl.locations[0].qty, 110.0)
|
||||
self.assertTrue(hasattr(pl, "locations"))
|
||||
|
||||
pl1.submit()
|
||||
|
||||
pl.reload()
|
||||
pl.submit()
|
||||
self.assertEqual(pl.locations[0].qty, 110.0)
|
||||
self.assertTrue(hasattr(pl, "locations"))
|
||||
|
||||
so = make_sales_order(item_code=item, qty=110, rate=100)
|
||||
pl = create_pick_list(so.name)
|
||||
pl.save()
|
||||
|
||||
self.assertEqual(pl.locations[0].qty, 80.0)
|
||||
|
||||
@@ -132,7 +132,8 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "quantity_section",
|
||||
@@ -240,7 +241,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-04 16:12:16.257951",
|
||||
"modified": "2024-05-07 15:32:42.905446",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Pick List Item",
|
||||
@@ -251,4 +252,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,8 +238,7 @@ class PurchaseReceipt(BuyingController):
|
||||
self.po_required()
|
||||
self.validate_items_quality_inspection()
|
||||
self.validate_with_previous_doc()
|
||||
self.validate_uom_is_integer("uom", ["qty", "received_qty"])
|
||||
self.validate_uom_is_integer("stock_uom", "stock_qty")
|
||||
self.validate_uom_is_integer()
|
||||
self.validate_cwip_accounts()
|
||||
self.validate_provisional_expense_account()
|
||||
|
||||
@@ -253,6 +252,10 @@ class PurchaseReceipt(BuyingController):
|
||||
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
|
||||
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
|
||||
|
||||
def validate_uom_is_integer(self):
|
||||
super().validate_uom_is_integer("uom", ["qty", "received_qty"], "Purchase Receipt Item")
|
||||
super().validate_uom_is_integer("stock_uom", "stock_qty", "Purchase Receipt Item")
|
||||
|
||||
def validate_cwip_accounts(self):
|
||||
for item in self.get("items"):
|
||||
if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category):
|
||||
|
||||
@@ -2968,6 +2968,7 @@ def make_purchase_receipt(**args):
|
||||
"serial_nos": serial_nos,
|
||||
"posting_date": args.posting_date or today(),
|
||||
"posting_time": args.posting_time,
|
||||
"do_not_submit": 1,
|
||||
}
|
||||
)
|
||||
).name
|
||||
|
||||
@@ -596,6 +596,13 @@ class SerialandBatchBundle(Document):
|
||||
|
||||
serial_batches = {}
|
||||
for row in self.entries:
|
||||
if not row.qty and row.batch_no and not row.serial_no:
|
||||
frappe.throw(
|
||||
_("At row {0}: Qty is mandatory for the batch {1}").format(
|
||||
bold(row.idx), bold(row.batch_no)
|
||||
)
|
||||
)
|
||||
|
||||
if self.has_serial_no and not row.serial_no:
|
||||
frappe.throw(
|
||||
_("At row {0}: Serial No is mandatory for Item {1}").format(
|
||||
@@ -831,7 +838,12 @@ class SerialandBatchBundle(Document):
|
||||
for batch in batches:
|
||||
frappe.db.set_value("Batch", batch.name, {"reference_name": None, "reference_doctype": None})
|
||||
|
||||
def validate_serial_and_batch_data(self):
|
||||
if not self.voucher_no:
|
||||
frappe.throw(_("Voucher No is mandatory"))
|
||||
|
||||
def before_submit(self):
|
||||
self.validate_serial_and_batch_data()
|
||||
self.validate_serial_and_batch_no_for_returned()
|
||||
self.set_purchase_document_no()
|
||||
|
||||
|
||||
@@ -1340,6 +1340,7 @@ erpnext.stock.select_batch_and_serial_no = (frm, item) => {
|
||||
frappe.model.set_value(item.doctype, item.name, {
|
||||
serial_and_batch_bundle: r.name,
|
||||
use_serial_batch_fields: 0,
|
||||
basic_rate: r.avg_rate,
|
||||
qty:
|
||||
Math.abs(r.total_qty) /
|
||||
flt(item.conversion_factor || 1, precision("conversion_factor", item)),
|
||||
|
||||
@@ -194,6 +194,7 @@ class StockEntry(StockController):
|
||||
if self.work_order:
|
||||
self.pro_doc = frappe.get_doc("Work Order", self.work_order)
|
||||
|
||||
self.validate_duplicate_serial_and_batch_bundle("items")
|
||||
self.validate_posting_time()
|
||||
self.validate_purpose()
|
||||
self.validate_item()
|
||||
|
||||
@@ -1483,7 +1483,7 @@ def create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list
|
||||
"posting_date": dn.posting_date,
|
||||
"posting_time": dn.posting_time,
|
||||
"voucher_type": "Delivery Note",
|
||||
"do_not_submit": dn.name,
|
||||
"do_not_submit": 1,
|
||||
}
|
||||
)
|
||||
).name
|
||||
|
||||
@@ -70,6 +70,7 @@ class StockReconciliation(StockController):
|
||||
self.validate_posting_time()
|
||||
self.set_current_serial_and_batch_bundle()
|
||||
self.set_new_serial_and_batch_bundle()
|
||||
self.validate_duplicate_serial_and_batch_bundle("items")
|
||||
self.remove_items_with_no_change()
|
||||
self.validate_data()
|
||||
self.validate_expense_account()
|
||||
@@ -822,11 +823,9 @@ class StockReconciliation(StockController):
|
||||
else:
|
||||
self._cancel()
|
||||
|
||||
def recalculate_current_qty(self, voucher_detail_no, sle_creation, add_new_sle=False):
|
||||
def recalculate_current_qty(self, voucher_detail_no):
|
||||
from erpnext.stock.stock_ledger import get_valuation_rate
|
||||
|
||||
sl_entries = []
|
||||
|
||||
for row in self.items:
|
||||
if voucher_detail_no != row.name:
|
||||
continue
|
||||
@@ -880,32 +879,6 @@ class StockReconciliation(StockController):
|
||||
}
|
||||
)
|
||||
|
||||
if (
|
||||
add_new_sle
|
||||
and not frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0},
|
||||
"name",
|
||||
)
|
||||
and (not row.current_serial_and_batch_bundle)
|
||||
):
|
||||
self.set_current_serial_and_batch_bundle(voucher_detail_no, save=True)
|
||||
row.reload()
|
||||
|
||||
if row.current_qty > 0 and row.current_serial_and_batch_bundle:
|
||||
new_sle = self.get_sle_for_items(row)
|
||||
new_sle.actual_qty = row.current_qty * -1
|
||||
new_sle.valuation_rate = row.current_valuation_rate
|
||||
new_sle.creation_time = add_to_date(sle_creation, seconds=-1)
|
||||
new_sle.serial_and_batch_bundle = row.current_serial_and_batch_bundle
|
||||
new_sle.qty_after_transaction = 0.0
|
||||
sl_entries.append(new_sle)
|
||||
|
||||
if sl_entries:
|
||||
self.make_sl_entries(sl_entries, allow_negative_stock=self.has_negative_stock_allowed())
|
||||
if not frappe.db.exists("Repost Item Valuation", {"voucher_no": self.name, "status": "Queued"}):
|
||||
self.repost_future_sle_and_gle(force=True)
|
||||
|
||||
def has_negative_stock_allowed(self):
|
||||
allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
|
||||
if allow_negative_stock:
|
||||
|
||||
@@ -756,66 +756,6 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
||||
|
||||
self.assertEqual(flt(sle[0].actual_qty), flt(-100.0))
|
||||
|
||||
def test_backdated_stock_reco_entry_with_batch(self):
|
||||
item_code = self.make_item(
|
||||
"Test New Batch Item ABCVSD",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"batch_number_series": "BNS9.####",
|
||||
"create_new_batch": 1,
|
||||
},
|
||||
).name
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
# Stock Reco for 100, Balace Qty 100
|
||||
stock_reco = create_stock_reconciliation(
|
||||
item_code=item_code,
|
||||
posting_date=nowdate(),
|
||||
posting_time="11:00:00",
|
||||
warehouse=warehouse,
|
||||
qty=100,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
sles = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=["actual_qty"],
|
||||
filters={"voucher_no": stock_reco.name, "is_cancelled": 0},
|
||||
)
|
||||
|
||||
self.assertEqual(len(sles), 1)
|
||||
|
||||
stock_reco.reload()
|
||||
batch_no = get_batch_from_bundle(stock_reco.items[0].serial_and_batch_bundle)
|
||||
|
||||
# Stock Reco for 100, Balace Qty 100
|
||||
stock_reco1 = create_stock_reconciliation(
|
||||
item_code=item_code,
|
||||
posting_date=add_days(nowdate(), -1),
|
||||
posting_time="11:00:00",
|
||||
batch_no=batch_no,
|
||||
warehouse=warehouse,
|
||||
qty=60,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
sles = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=["actual_qty"],
|
||||
filters={"voucher_no": stock_reco.name, "is_cancelled": 0},
|
||||
)
|
||||
|
||||
stock_reco1.reload()
|
||||
get_batch_from_bundle(stock_reco1.items[0].serial_and_batch_bundle)
|
||||
|
||||
self.assertEqual(len(sles), 2)
|
||||
|
||||
for row in sles:
|
||||
if row.actual_qty < 0:
|
||||
self.assertEqual(row.actual_qty, -60)
|
||||
|
||||
def test_update_stock_reconciliation_while_reposting(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
@@ -986,6 +926,150 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
||||
active_serial_no = frappe.get_all("Serial No", filters={"status": "Active", "item_code": item_code})
|
||||
self.assertEqual(len(active_serial_no), 5)
|
||||
|
||||
def test_balance_qty_for_batch_with_backdated_stock_reco_and_future_entries(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
item = self.make_item(
|
||||
"Test Batch Item Original Test",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TEST-BATCH-SRWFEE-.###",
|
||||
},
|
||||
)
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
se1 = make_stock_entry(
|
||||
item_code=item.name,
|
||||
target=warehouse,
|
||||
qty=50,
|
||||
basic_rate=100,
|
||||
posting_date=add_days(nowdate(), -2),
|
||||
)
|
||||
batch1 = get_batch_from_bundle(se1.items[0].serial_and_batch_bundle)
|
||||
|
||||
se2 = make_stock_entry(
|
||||
item_code=item.name,
|
||||
target=warehouse,
|
||||
qty=50,
|
||||
basic_rate=100,
|
||||
posting_date=add_days(nowdate(), -2),
|
||||
)
|
||||
batch2 = get_batch_from_bundle(se2.items[0].serial_and_batch_bundle)
|
||||
|
||||
se3 = make_stock_entry(
|
||||
item_code=item.name,
|
||||
target=warehouse,
|
||||
qty=100,
|
||||
basic_rate=100,
|
||||
posting_date=add_days(nowdate(), -2),
|
||||
)
|
||||
batch3 = get_batch_from_bundle(se3.items[0].serial_and_batch_bundle)
|
||||
|
||||
se3 = make_stock_entry(
|
||||
item_code=item.name,
|
||||
target=warehouse,
|
||||
qty=100,
|
||||
basic_rate=100,
|
||||
posting_date=nowdate(),
|
||||
)
|
||||
|
||||
sle = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={
|
||||
"item_code": item.name,
|
||||
"warehouse": warehouse,
|
||||
"is_cancelled": 0,
|
||||
"voucher_no": se3.name,
|
||||
},
|
||||
fields=["qty_after_transaction"],
|
||||
order_by="posting_time desc, creation desc",
|
||||
)
|
||||
|
||||
self.assertEqual(flt(sle[0].qty_after_transaction), flt(300.0))
|
||||
|
||||
sr = create_stock_reconciliation(
|
||||
item_code=item.name,
|
||||
warehouse=warehouse,
|
||||
qty=0,
|
||||
batch_no=batch1,
|
||||
posting_date=add_days(nowdate(), -1),
|
||||
use_serial_batch_fields=1,
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
for batch in [batch2, batch3]:
|
||||
sr.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": item.name,
|
||||
"warehouse": warehouse,
|
||||
"qty": 0,
|
||||
"batch_no": batch,
|
||||
"use_serial_batch_fields": 1,
|
||||
},
|
||||
)
|
||||
|
||||
sr.save()
|
||||
sr.submit()
|
||||
|
||||
sle = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={
|
||||
"item_code": item.name,
|
||||
"warehouse": warehouse,
|
||||
"is_cancelled": 0,
|
||||
"voucher_no": se3.name,
|
||||
},
|
||||
fields=["qty_after_transaction"],
|
||||
order_by="posting_time desc, creation desc",
|
||||
)
|
||||
|
||||
self.assertEqual(flt(sle[0].qty_after_transaction), flt(100.0))
|
||||
|
||||
def test_stock_reco_and_backdated_purchase_receipt(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
item = self.make_item(
|
||||
"Test Batch Item Original STOCK RECO Test",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TEST-BATCH-SRCOSRWFEE-.###",
|
||||
},
|
||||
)
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
sr = create_stock_reconciliation(
|
||||
item_code=item.name,
|
||||
warehouse=warehouse,
|
||||
qty=100,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
sr.reload()
|
||||
self.assertTrue(sr.items[0].serial_and_batch_bundle)
|
||||
self.assertFalse(sr.items[0].current_serial_and_batch_bundle)
|
||||
batch = get_batch_from_bundle(sr.items[0].serial_and_batch_bundle)
|
||||
|
||||
se1 = make_stock_entry(
|
||||
item_code=item.name,
|
||||
target=warehouse,
|
||||
qty=50,
|
||||
basic_rate=100,
|
||||
posting_date=add_days(nowdate(), -2),
|
||||
)
|
||||
|
||||
batch1 = get_batch_from_bundle(se1.items[0].serial_and_batch_bundle)
|
||||
self.assertFalse(batch1 == batch)
|
||||
|
||||
sr.reload()
|
||||
self.assertTrue(sr.items[0].serial_and_batch_bundle)
|
||||
self.assertFalse(sr.items[0].current_serial_and_batch_bundle)
|
||||
|
||||
|
||||
def create_batch_item_with_batch(item_name, batch_id):
|
||||
batch_item_doc = create_item(item_name, is_stock_item=1)
|
||||
@@ -1085,7 +1169,7 @@ def create_stock_reconciliation(**args):
|
||||
)
|
||||
|
||||
bundle_id = None
|
||||
if not args.use_serial_batch_fields and (args.batch_no or args.serial_no):
|
||||
if not args.use_serial_batch_fields and (args.batch_no or args.serial_no) and args.qty:
|
||||
batches = frappe._dict({})
|
||||
if args.batch_no:
|
||||
batches[args.batch_no] = args.qty
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"end_time",
|
||||
"limits_dont_apply_on",
|
||||
"item_based_reposting",
|
||||
"do_reposting_for_each_stock_transaction",
|
||||
"errors_notification_section",
|
||||
"notify_reposting_error_to_role"
|
||||
],
|
||||
@@ -65,12 +66,18 @@
|
||||
"fieldname": "errors_notification_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Errors Notification"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "do_reposting_for_each_stock_transaction",
|
||||
"fieldtype": "Check",
|
||||
"label": "Do reposting for each Stock Transaction"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-01 16:14:29.080697",
|
||||
"modified": "2024-04-24 12:19:40.204888",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Reposting Settings",
|
||||
@@ -91,4 +98,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ class StockRepostingSettings(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
do_reposting_for_each_stock_transaction: DF.Check
|
||||
end_time: DF.Time | None
|
||||
item_based_reposting: DF.Check
|
||||
limit_reposting_timeslot: DF.Check
|
||||
@@ -29,6 +30,10 @@ class StockRepostingSettings(Document):
|
||||
def validate(self):
|
||||
self.set_minimum_reposting_time_slot()
|
||||
|
||||
def before_save(self):
|
||||
if self.do_reposting_for_each_stock_transaction:
|
||||
self.item_based_reposting = 1
|
||||
|
||||
def set_minimum_reposting_time_slot(self):
|
||||
"""Ensure that timeslot for reposting is at least 12 hours."""
|
||||
if not self.limit_reposting_timeslot:
|
||||
|
||||
@@ -38,3 +38,51 @@ class TestStockRepostingSettings(unittest.TestCase):
|
||||
|
||||
users = get_recipients()
|
||||
self.assertTrue(user in users)
|
||||
|
||||
def test_do_reposting_for_each_stock_transaction(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 1)
|
||||
if frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"):
|
||||
frappe.db.set_single_value("Stock Reposting Settings", "item_based_reposting", 0)
|
||||
|
||||
item = make_item(
|
||||
"_Test item for reposting check for each transaction", properties={"is_stock_item": 1}
|
||||
).name
|
||||
|
||||
stock_entry = make_stock_entry(
|
||||
item_code=item,
|
||||
qty=1,
|
||||
rate=100,
|
||||
stock_entry_type="Material Receipt",
|
||||
target="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
riv = frappe.get_all("Repost Item Valuation", filters={"voucher_no": stock_entry.name}, pluck="name")
|
||||
self.assertTrue(riv)
|
||||
|
||||
frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 0)
|
||||
|
||||
def test_do_not_reposting_for_each_stock_transaction(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 0)
|
||||
if frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"):
|
||||
frappe.db.set_single_value("Stock Reposting Settings", "item_based_reposting", 0)
|
||||
|
||||
item = make_item(
|
||||
"_Test item for do not reposting check for each transaction", properties={"is_stock_item": 1}
|
||||
).name
|
||||
|
||||
stock_entry = make_stock_entry(
|
||||
item_code=item,
|
||||
qty=1,
|
||||
rate=100,
|
||||
stock_entry_type="Material Receipt",
|
||||
target="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
riv = frappe.get_all("Repost Item Valuation", filters={"voucher_no": stock_entry.name}, pluck="name")
|
||||
self.assertFalse(riv)
|
||||
|
||||
@@ -838,7 +838,12 @@ def insert_item_price(args):
|
||||
|
||||
item_price = frappe.db.get_value(
|
||||
"Item Price",
|
||||
{"item_code": args.item_code, "price_list": args.price_list, "currency": args.currency},
|
||||
{
|
||||
"item_code": args.item_code,
|
||||
"price_list": args.price_list,
|
||||
"currency": args.currency,
|
||||
"uom": args.stock_uom,
|
||||
},
|
||||
["name", "price_list_rate"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Available Batch Report"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
width: "80",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_default("company"),
|
||||
},
|
||||
{
|
||||
fieldname: "to_date",
|
||||
label: __("On This Date"),
|
||||
fieldtype: "Date",
|
||||
width: "80",
|
||||
reqd: 1,
|
||||
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
||||
},
|
||||
{
|
||||
fieldname: "item_code",
|
||||
label: __("Item"),
|
||||
fieldtype: "Link",
|
||||
width: "80",
|
||||
options: "Item",
|
||||
get_query: () => {
|
||||
return {
|
||||
filters: {
|
||||
has_batch_no: 1,
|
||||
disabled: 0,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "warehouse",
|
||||
label: __("Warehouse"),
|
||||
fieldtype: "Link",
|
||||
width: "80",
|
||||
options: "Warehouse",
|
||||
get_query: () => {
|
||||
let warehouse_type = frappe.query_report.get_filter_value("warehouse_type");
|
||||
let company = frappe.query_report.get_filter_value("company");
|
||||
|
||||
return {
|
||||
filters: {
|
||||
...(warehouse_type && { warehouse_type }),
|
||||
...(company && { company }),
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "warehouse_type",
|
||||
label: __("Warehouse Type"),
|
||||
fieldtype: "Link",
|
||||
width: "80",
|
||||
options: "Warehouse Type",
|
||||
},
|
||||
{
|
||||
fieldname: "batch_no",
|
||||
label: __("Batch No"),
|
||||
fieldtype: "Link",
|
||||
width: "80",
|
||||
options: "Batch",
|
||||
get_query: () => {
|
||||
let item = frappe.query_report.get_filter_value("item_code");
|
||||
|
||||
return {
|
||||
filters: {
|
||||
...(item && { item }),
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "include_expired_batches",
|
||||
label: __("Include Expired Batches"),
|
||||
fieldtype: "Check",
|
||||
width: "80",
|
||||
},
|
||||
{
|
||||
fieldname: "show_item_name",
|
||||
label: __("Show Item Name"),
|
||||
fieldtype: "Check",
|
||||
width: "80",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"columns": [],
|
||||
"creation": "2024-04-11 17:03:32.253275",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"json": "{}",
|
||||
"letter_head": "",
|
||||
"letterhead": null,
|
||||
"modified": "2024-04-23 17:18:19.779036",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Available Batch Report",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Stock Ledger Entry",
|
||||
"report_name": "Available Batch Report",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Stock User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import flt, today
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
columns, data = [], []
|
||||
data = get_data(filters)
|
||||
columns = get_columns(filters)
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
columns = [
|
||||
{
|
||||
"label": _("Item Code"),
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"options": "Item",
|
||||
"width": 200,
|
||||
}
|
||||
]
|
||||
|
||||
if filters.show_item_name:
|
||||
columns.append(
|
||||
{
|
||||
"label": _("Item Name"),
|
||||
"fieldname": "item_name",
|
||||
"fieldtype": "Link",
|
||||
"options": "Item",
|
||||
"width": 200,
|
||||
}
|
||||
)
|
||||
|
||||
columns.extend(
|
||||
[
|
||||
{
|
||||
"label": _("Warehouse"),
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"options": "Warehouse",
|
||||
"width": 200,
|
||||
},
|
||||
{
|
||||
"label": _("Batch No"),
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"width": 150,
|
||||
"options": "Batch",
|
||||
},
|
||||
{"label": _("Balance Qty"), "fieldname": "balance_qty", "fieldtype": "Float", "width": 150},
|
||||
]
|
||||
)
|
||||
|
||||
return columns
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
data = []
|
||||
batchwise_data = get_batchwise_data_from_stock_ledger(filters)
|
||||
batchwise_data = get_batchwise_data_from_serial_batch_bundle(batchwise_data, filters)
|
||||
|
||||
data = parse_batchwise_data(batchwise_data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def parse_batchwise_data(batchwise_data):
|
||||
data = []
|
||||
for key in batchwise_data:
|
||||
d = batchwise_data[key]
|
||||
if d.balance_qty == 0:
|
||||
continue
|
||||
|
||||
data.append(d)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_batchwise_data_from_stock_ledger(filters):
|
||||
batchwise_data = frappe._dict({})
|
||||
|
||||
table = frappe.qb.DocType("Stock Ledger Entry")
|
||||
batch = frappe.qb.DocType("Batch")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.inner_join(batch)
|
||||
.on(table.batch_no == batch.name)
|
||||
.select(
|
||||
table.item_code,
|
||||
table.batch_no,
|
||||
table.warehouse,
|
||||
Sum(table.actual_qty).as_("balance_qty"),
|
||||
)
|
||||
.where(table.is_cancelled == 0)
|
||||
.groupby(table.batch_no, table.item_code, table.warehouse)
|
||||
)
|
||||
|
||||
query = get_query_based_on_filters(query, batch, table, filters)
|
||||
|
||||
for d in query.run(as_dict=True):
|
||||
key = (d.item_code, d.warehouse, d.batch_no)
|
||||
batchwise_data.setdefault(key, d)
|
||||
|
||||
return batchwise_data
|
||||
|
||||
|
||||
def get_batchwise_data_from_serial_batch_bundle(batchwise_data, filters):
|
||||
table = frappe.qb.DocType("Stock Ledger Entry")
|
||||
ch_table = frappe.qb.DocType("Serial and Batch Entry")
|
||||
batch = frappe.qb.DocType("Batch")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.inner_join(ch_table)
|
||||
.on(table.serial_and_batch_bundle == ch_table.parent)
|
||||
.inner_join(batch)
|
||||
.on(ch_table.batch_no == batch.name)
|
||||
.select(
|
||||
table.item_code,
|
||||
ch_table.batch_no,
|
||||
table.warehouse,
|
||||
Sum(ch_table.qty).as_("balance_qty"),
|
||||
)
|
||||
.where((table.is_cancelled == 0) & (table.docstatus == 1))
|
||||
.groupby(ch_table.batch_no, table.item_code, ch_table.warehouse)
|
||||
)
|
||||
|
||||
query = get_query_based_on_filters(query, batch, table, filters)
|
||||
|
||||
for d in query.run(as_dict=True):
|
||||
key = (d.item_code, d.warehouse, d.batch_no)
|
||||
if key in batchwise_data:
|
||||
batchwise_data[key].balance_qty += flt(d.balance_qty)
|
||||
else:
|
||||
batchwise_data.setdefault(key, d)
|
||||
|
||||
return batchwise_data
|
||||
|
||||
|
||||
def get_query_based_on_filters(query, batch, table, filters):
|
||||
if filters.item_code:
|
||||
query = query.where(table.item_code == filters.item_code)
|
||||
|
||||
if filters.batch_no:
|
||||
query = query.where(batch.name == filters.batch_no)
|
||||
|
||||
if not filters.include_expired_batches:
|
||||
query = query.where((batch.expiry_date >= today()) | (batch.expiry_date.isnull()))
|
||||
if filters.to_date == today():
|
||||
query = query.where(batch.batch_qty > 0)
|
||||
|
||||
if filters.warehouse:
|
||||
lft, rgt = frappe.db.get_value("Warehouse", filters.warehouse, ["lft", "rgt"])
|
||||
warehouses = frappe.get_all(
|
||||
"Warehouse", filters={"lft": (">=", lft), "rgt": ("<=", rgt), "is_group": 0}, pluck="name"
|
||||
)
|
||||
|
||||
query = query.where(table.warehouse.isin(warehouses))
|
||||
|
||||
elif filters.warehouse_type:
|
||||
warehouses = frappe.get_all(
|
||||
"Warehouse", filters={"warehouse_type": filters.warehouse_type, "is_group": 0}, pluck="name"
|
||||
)
|
||||
|
||||
query = query.where(table.warehouse.isin(warehouses))
|
||||
|
||||
if filters.show_item_name:
|
||||
query = query.select(batch.item_name)
|
||||
|
||||
return query
|
||||
@@ -40,16 +40,25 @@ frappe.query_reports["Batch-Wise Balance History"] = {
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "warehouse_type",
|
||||
label: __("Warehouse Type"),
|
||||
fieldtype: "Link",
|
||||
width: "80",
|
||||
options: "Warehouse Type",
|
||||
},
|
||||
{
|
||||
fieldname: "warehouse",
|
||||
label: __("Warehouse"),
|
||||
fieldtype: "Link",
|
||||
options: "Warehouse",
|
||||
get_query: function () {
|
||||
let warehouse_type = frappe.query_report.get_filter_value("warehouse_type");
|
||||
let company = frappe.query_report.get_filter_value("company");
|
||||
return {
|
||||
filters: {
|
||||
company: company,
|
||||
...(warehouse_type && { warehouse_type }),
|
||||
...(company && { company }),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -30,8 +30,15 @@ def execute(filters=None):
|
||||
|
||||
sle_count = _estimate_table_row_count("Stock Ledger Entry")
|
||||
|
||||
if sle_count > SLE_COUNT_LIMIT and not filters.get("item_code") and not filters.get("warehouse"):
|
||||
frappe.throw(_("Please select either the Item or Warehouse filter to generate the report."))
|
||||
if (
|
||||
sle_count > SLE_COUNT_LIMIT
|
||||
and not filters.get("item_code")
|
||||
and not filters.get("warehouse")
|
||||
and not filters.get("warehouse_type")
|
||||
):
|
||||
frappe.throw(
|
||||
_("Please select either the Item or Warehouse or Warehouse Type filter to generate the report.")
|
||||
)
|
||||
|
||||
if filters.from_date > filters.to_date:
|
||||
frappe.throw(_("From Date must be before To Date"))
|
||||
@@ -121,6 +128,16 @@ def get_stock_ledger_entries_for_batch_no(filters):
|
||||
)
|
||||
|
||||
query = apply_warehouse_filter(query, sle, filters)
|
||||
if filters.warehouse_type and not filters.warehouse:
|
||||
warehouses = frappe.get_all(
|
||||
"Warehouse",
|
||||
filters={"warehouse_type": filters.warehouse_type, "is_group": 0},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
if warehouses:
|
||||
query = query.where(sle.warehouse.isin(warehouses))
|
||||
|
||||
for field in ["item_code", "batch_no", "company"]:
|
||||
if filters.get(field):
|
||||
query = query.where(sle[field] == filters.get(field))
|
||||
@@ -154,6 +171,16 @@ def get_stock_ledger_entries_for_batch_bundle(filters):
|
||||
)
|
||||
|
||||
query = apply_warehouse_filter(query, sle, filters)
|
||||
if filters.warehouse_type and not filters.warehouse:
|
||||
warehouses = frappe.get_all(
|
||||
"Warehouse",
|
||||
filters={"warehouse_type": filters.warehouse_type, "is_group": 0},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
if warehouses:
|
||||
query = query.where(sle.warehouse.isin(warehouses))
|
||||
|
||||
for field in ["item_code", "batch_no", "company"]:
|
||||
if filters.get(field):
|
||||
if field == "batch_no":
|
||||
|
||||
@@ -18,15 +18,24 @@ frappe.query_reports["Stock Ageing"] = {
|
||||
default: frappe.datetime.get_today(),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "warehouse_type",
|
||||
label: __("Warehouse Type"),
|
||||
fieldtype: "Link",
|
||||
width: "80",
|
||||
options: "Warehouse Type",
|
||||
},
|
||||
{
|
||||
fieldname: "warehouse",
|
||||
label: __("Warehouse"),
|
||||
fieldtype: "Link",
|
||||
options: "Warehouse",
|
||||
get_query: () => {
|
||||
const company = frappe.query_report.get_filter_value("company");
|
||||
let warehouse_type = frappe.query_report.get_filter_value("warehouse_type");
|
||||
let company = frappe.query_report.get_filter_value("company");
|
||||
return {
|
||||
filters: {
|
||||
...(warehouse_type && { warehouse_type }),
|
||||
...(company && { company }),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -434,6 +434,15 @@ class FIFOSlots:
|
||||
|
||||
if self.filters.get("warehouse"):
|
||||
sle_query = self.__get_warehouse_conditions(sle, sle_query)
|
||||
elif self.filters.get("warehouse_type"):
|
||||
warehouses = frappe.get_all(
|
||||
"Warehouse",
|
||||
filters={"warehouse_type": self.filters.get("warehouse_type"), "is_group": 0},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
if warehouses:
|
||||
sle_query = sle_query.where(sle.warehouse.isin(warehouses))
|
||||
|
||||
sle_query = sle_query.orderby(sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty)
|
||||
|
||||
|
||||
@@ -146,6 +146,8 @@ class StockBalanceReport:
|
||||
if self.filters.get("show_stock_ageing_data"):
|
||||
self.sle_entries = self.sle_query.run(as_dict=True)
|
||||
|
||||
# HACK: This is required to avoid causing db query in flt
|
||||
_system_settings = frappe.get_cached_doc("System Settings")
|
||||
with frappe.db.unbuffered_cursor():
|
||||
if not self.filters.get("show_stock_ageing_data"):
|
||||
self.sle_entries = self.sle_query.run(as_dict=True, as_iterator=True)
|
||||
|
||||
@@ -61,6 +61,8 @@ def execute(filters=None):
|
||||
actual_qty += flt(sle.actual_qty, precision)
|
||||
stock_value += sle.stock_value_difference
|
||||
batch_balance_dict[sle.batch_no] += sle.actual_qty
|
||||
if filters.get("segregate_serial_batch_bundle"):
|
||||
actual_qty = batch_balance_dict[sle.batch_no]
|
||||
|
||||
if sle.voucher_type == "Stock Reconciliation" and not sle.actual_qty:
|
||||
actual_qty = sle.qty_after_transaction
|
||||
@@ -229,13 +231,6 @@ def get_columns(filters):
|
||||
"width": 100,
|
||||
"convertible": "qty",
|
||||
},
|
||||
{
|
||||
"label": _("Voucher #"),
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "voucher_type",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Warehouse"),
|
||||
"fieldname": "warehouse",
|
||||
|
||||
@@ -56,13 +56,14 @@ def execute(filters=None):
|
||||
item_value.setdefault((item, item_map[item]["item_group"]), [])
|
||||
item_value[(item, item_map[item]["item_group"])].append(total_stock_value)
|
||||
|
||||
itemwise_brand = frappe._dict(get_itemwise_brand(items))
|
||||
# sum bal_qty by item
|
||||
for (item, item_group), wh_balance in item_balance.items():
|
||||
if not item_ageing.get(item):
|
||||
continue
|
||||
|
||||
total_stock_value = sum(item_value[(item, item_group)])
|
||||
row = [item, item_map[item]["item_name"], item_group, total_stock_value]
|
||||
row = [item, item_map[item]["item_name"], item_group, itemwise_brand.get(item), total_stock_value]
|
||||
|
||||
fifo_queue = item_ageing[item]["fifo_queue"]
|
||||
average_age = 0.00
|
||||
@@ -85,6 +86,10 @@ def execute(filters=None):
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_itemwise_brand(items):
|
||||
return frappe.get_all("Item", filters={"name": ("in", items)}, fields=["name", "brand"], as_list=1)
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
"""return columns"""
|
||||
|
||||
@@ -92,6 +97,7 @@ def get_columns(filters):
|
||||
_("Item") + ":Link/Item:150",
|
||||
_("Item Name") + ":Link/Item:150",
|
||||
_("Item Group") + "::120",
|
||||
_("Brand") + ":Link/Brand:120",
|
||||
_("Value") + ":Currency:120",
|
||||
_("Age") + ":Float:120",
|
||||
]
|
||||
|
||||
@@ -840,7 +840,7 @@ class update_entries_after:
|
||||
|
||||
def reset_actual_qty_for_stock_reco(self, sle):
|
||||
doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
|
||||
doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
|
||||
doc.recalculate_current_qty(sle.voucher_detail_no)
|
||||
|
||||
if sle.actual_qty < 0:
|
||||
sle.actual_qty = (
|
||||
@@ -1728,6 +1728,10 @@ def get_stock_reco_qty_shift(args):
|
||||
stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
|
||||
else:
|
||||
stock_reco_qty_shift = flt(args.actual_qty)
|
||||
|
||||
elif args.get("serial_and_batch_bundle"):
|
||||
stock_reco_qty_shift = flt(args.actual_qty)
|
||||
|
||||
else:
|
||||
# reco is being submitted
|
||||
last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get(
|
||||
@@ -1799,7 +1803,16 @@ def get_datetime_limit_condition(detail):
|
||||
def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
|
||||
if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
|
||||
return
|
||||
if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
|
||||
|
||||
if (
|
||||
args.voucher_type == "Stock Reconciliation"
|
||||
and args.actual_qty < 0
|
||||
and args.get("serial_and_batch_bundle")
|
||||
and frappe.db.get_value("Stock Reconciliation Item", args.voucher_detail_no, "qty") > 0
|
||||
):
|
||||
return
|
||||
|
||||
if args.actual_qty >= 0 and args.voucher_type != "Stock Reconciliation":
|
||||
return
|
||||
|
||||
neg_sle = get_future_sle_with_negative_qty(args)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Married,既婚,
|
||||
|
@@ -30,8 +30,8 @@ class TransactionBase(StatusUpdater):
|
||||
except ValueError:
|
||||
frappe.throw(_("Invalid Posting Time"))
|
||||
|
||||
def validate_uom_is_integer(self, uom_field, qty_fields):
|
||||
validate_uom_is_integer(self, uom_field, qty_fields)
|
||||
def validate_uom_is_integer(self, uom_field, qty_fields, child_dt=None):
|
||||
validate_uom_is_integer(self, uom_field, qty_fields, child_dt)
|
||||
|
||||
def validate_with_previous_doc(self, ref):
|
||||
self.exclude_fields = ["conversion_factor", "uom"] if self.get("is_return") else []
|
||||
@@ -210,12 +210,13 @@ def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None):
|
||||
for f in qty_fields:
|
||||
qty = d.get(f)
|
||||
if qty:
|
||||
if abs(cint(qty) - flt(qty, d.precision(f))) > 0.0000001:
|
||||
precision = d.precision(f)
|
||||
if abs(cint(qty) - flt(qty, precision)) > 0.0000001:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}."
|
||||
).format(
|
||||
flt(qty, d.precision(f)),
|
||||
flt(qty, precision),
|
||||
d.idx,
|
||||
frappe.bold(_("Must be Whole Number")),
|
||||
frappe.bold(d.get(uom_field)),
|
||||
|
||||
@@ -67,3 +67,9 @@ typing-modules = ["frappe.types.DF"]
|
||||
quote-style = "double"
|
||||
indent-style = "tab"
|
||||
docstring-code-format = true
|
||||
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://erpnext.com/"
|
||||
Repository = "https://github.com/frappe/erpnext.git"
|
||||
"Bug Reports" = "https://github.com/frappe/erpnext/issues"
|
||||
|
||||
Reference in New Issue
Block a user