mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-20 03:12:32 +00:00
Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fb035fe1b | ||
|
|
f3be4cd2b3 | ||
|
|
15bb41d3c2 | ||
|
|
410990b2a3 | ||
|
|
51a2b74db9 | ||
|
|
bfc34939f2 | ||
|
|
90d6e550aa | ||
|
|
4f5af0ff9f | ||
|
|
ab21b518e1 | ||
|
|
d4e2adbdb2 | ||
|
|
85e0e8110a | ||
|
|
791e4269d2 | ||
|
|
3df5aa04b5 | ||
|
|
05d5c48d29 | ||
|
|
2b736b52d6 | ||
|
|
24954b909b | ||
|
|
ed5affe25a | ||
|
|
8591a151de | ||
|
|
b2a0e4b810 | ||
|
|
aee03fe2ef | ||
|
|
62702b8bd6 | ||
|
|
cfb36cc9a8 | ||
|
|
3b4575af3d | ||
|
|
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 | ||
|
|
00d45c34ba | ||
|
|
44610621ab | ||
|
|
5f41036f4a | ||
|
|
e278688a4b | ||
|
|
088aa94fa1 | ||
|
|
c34b814c16 | ||
|
|
881dc02349 | ||
|
|
b27ad76557 | ||
|
|
40d059c7c1 | ||
|
|
52b42dfc14 | ||
|
|
dcfc768d33 | ||
|
|
ccdbad9f90 | ||
|
|
f59e43320b | ||
|
|
ec337d3465 | ||
|
|
59950290a2 | ||
|
|
361d7f1ba5 | ||
|
|
80adafc207 | ||
|
|
641b2a4705 | ||
|
|
e287376cc8 | ||
|
|
21dfb68763 | ||
|
|
1c28ed4d5b | ||
|
|
10059309da | ||
|
|
0ee91a2e54 | ||
|
|
fea906b883 | ||
|
|
128391be0a | ||
|
|
2febb2965e | ||
|
|
e07557c9c5 | ||
|
|
7d12674430 | ||
|
|
4342b891eb | ||
|
|
2d94ea074c | ||
|
|
4dbeabc8ef | ||
|
|
7711b13766 | ||
|
|
3d6ba6f21f | ||
|
|
9abc71f9c8 | ||
|
|
52ee6dcaed | ||
|
|
5de9b6ac75 | ||
|
|
14a1a18243 | ||
|
|
3762259d3d | ||
|
|
0053d57ec4 | ||
|
|
c24d9675e8 | ||
|
|
e90b60b5cb | ||
|
|
5d2ac5b3e9 | ||
|
|
72fc0836e1 | ||
|
|
ea8c57263b | ||
|
|
83a5284ba9 | ||
|
|
af0679076b | ||
|
|
e9c6c5a8eb | ||
|
|
4e215c6b7b |
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "15.20.0"
|
||||
__version__ = "15.21.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -139,6 +139,10 @@ class Dunning(AccountsController):
|
||||
)
|
||||
row.dunning_level = len(past_dunnings) + 1
|
||||
|
||||
def on_cancel(self):
|
||||
super().on_cancel()
|
||||
self.ignore_linked_doctypes = ["GL Entry"]
|
||||
|
||||
|
||||
def resolve_dunning(doc, state):
|
||||
"""
|
||||
|
||||
@@ -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], [])
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -2028,6 +2028,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 +2041,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,
|
||||
)
|
||||
@@ -1684,6 +1685,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: {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -91,7 +91,7 @@ class PaymentRequest(Document):
|
||||
self.status = "Draft"
|
||||
self.validate_reference_document()
|
||||
self.validate_payment_request_amount()
|
||||
self.validate_currency()
|
||||
# self.validate_currency()
|
||||
self.validate_subscription_details()
|
||||
|
||||
def validate_reference_document(self):
|
||||
@@ -330,21 +330,17 @@ class PaymentRequest(Document):
|
||||
}
|
||||
)
|
||||
|
||||
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
|
||||
amount = payment_entry.base_paid_amount
|
||||
else:
|
||||
amount = self.grand_total
|
||||
|
||||
payment_entry.received_amount = amount
|
||||
payment_entry.get("references")[0].allocated_amount = amount
|
||||
|
||||
for dimension in get_accounting_dimensions():
|
||||
payment_entry.update({dimension: self.get(dimension)})
|
||||
|
||||
if payment_entry.difference_amount:
|
||||
company_details = get_company_defaults(ref_doc.company)
|
||||
|
||||
payment_entry.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": company_details.exchange_gain_loss_account,
|
||||
"cost_center": company_details.cost_center,
|
||||
"amount": payment_entry.difference_amount,
|
||||
},
|
||||
)
|
||||
|
||||
if submit:
|
||||
payment_entry.insert(ignore_permissions=True)
|
||||
payment_entry.submit()
|
||||
@@ -463,6 +459,12 @@ def make_payment_request(**args):
|
||||
pr = frappe.get_doc("Payment Request", draft_payment_request)
|
||||
else:
|
||||
pr = frappe.new_doc("Payment Request")
|
||||
|
||||
if not args.get("payment_request_type"):
|
||||
args["payment_request_type"] = (
|
||||
"Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward"
|
||||
)
|
||||
|
||||
pr.update(
|
||||
{
|
||||
"payment_gateway_account": gateway_account.get("name"),
|
||||
@@ -521,9 +523,9 @@ def get_amount(ref_doc, payment_account=None):
|
||||
elif dt in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if not ref_doc.get("is_pos"):
|
||||
if ref_doc.party_account_currency == ref_doc.currency:
|
||||
grand_total = flt(ref_doc.outstanding_amount)
|
||||
grand_total = flt(ref_doc.grand_total)
|
||||
else:
|
||||
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
|
||||
grand_total = flt(ref_doc.base_grand_total) / ref_doc.conversion_rate
|
||||
elif dt == "Sales Invoice":
|
||||
for pay in ref_doc.payments:
|
||||
if pay.type == "Phone" and pay.account == payment_account:
|
||||
|
||||
@@ -86,6 +86,8 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
pr = make_payment_request(
|
||||
dt="Purchase Invoice",
|
||||
dn=si_usd.name,
|
||||
party_type="Supplier",
|
||||
party="_Test Supplier USD",
|
||||
recipient_id="user@example.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
@@ -98,6 +100,51 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(pr.status, "Paid")
|
||||
|
||||
def test_multiple_payment_entry_against_purchase_invoice(self):
|
||||
purchase_invoice = make_purchase_invoice(
|
||||
customer="_Test Supplier USD",
|
||||
debit_to="_Test Payable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=50,
|
||||
)
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Purchase Invoice",
|
||||
party_type="Supplier",
|
||||
party="_Test Supplier USD",
|
||||
dn=purchase_invoice.name,
|
||||
recipient_id="user@example.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
pr.grand_total = pr.grand_total / 2
|
||||
|
||||
pr.submit()
|
||||
pr.create_payment_entry()
|
||||
|
||||
purchase_invoice.load_from_db()
|
||||
self.assertEqual(purchase_invoice.status, "Partly Paid")
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Purchase Invoice",
|
||||
party_type="Supplier",
|
||||
party="_Test Supplier USD",
|
||||
dn=purchase_invoice.name,
|
||||
recipient_id="user@example.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
pr.save()
|
||||
pr.submit()
|
||||
pr.create_payment_entry()
|
||||
|
||||
purchase_invoice.load_from_db()
|
||||
self.assertEqual(purchase_invoice.status, "Paid")
|
||||
|
||||
def test_payment_entry(self):
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -460,7 +460,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
|
||||
for gle in gl_entries:
|
||||
group_by_value = gle.get(group_by)
|
||||
gle.voucher_type = _(gle.voucher_type)
|
||||
gle.voucher_type = gle.voucher_type
|
||||
|
||||
if gle.posting_date < from_date or (cstr(gle.is_opening) == "Yes" and not show_opening_entries):
|
||||
if not group_by_voucher_consolidated:
|
||||
|
||||
@@ -655,13 +655,13 @@ class GrossProfitGenerator:
|
||||
elif self.delivery_notes.get((row.parent, row.item_code), None):
|
||||
# check if Invoice has delivery notes
|
||||
dn = self.delivery_notes.get((row.parent, row.item_code))
|
||||
parenttype, parent, item_row, _warehouse = (
|
||||
parenttype, parent, item_row, dn_warehouse = (
|
||||
"Delivery Note",
|
||||
dn["delivery_note"],
|
||||
dn["item_row"],
|
||||
dn["warehouse"],
|
||||
)
|
||||
my_sle = self.get_stock_ledger_entries(item_code, row.warehouse)
|
||||
my_sle = self.get_stock_ledger_entries(item_code, dn_warehouse)
|
||||
return self.calculate_buying_amount_from_sle(
|
||||
row, my_sle, parenttype, parent, item_row, item_code
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -1437,7 +1437,8 @@ class AccountsController(TransactionBase):
|
||||
|
||||
dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit"
|
||||
|
||||
if d.reference_doctype == "Purchase Invoice":
|
||||
# Inverse debit/credit for payable accounts
|
||||
if self.is_payable_account(d.reference_doctype, party_account):
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
@@ -1471,6 +1472,14 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
)
|
||||
|
||||
def is_payable_account(self, reference_doctype, account):
|
||||
if reference_doctype == "Purchase Invoice" or (
|
||||
reference_doctype == "Journal Entry"
|
||||
and frappe.get_cached_value("Account", account, "account_type") == "Payable"
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
def update_against_document_in_jv(self):
|
||||
"""
|
||||
Links invoice and advance voucher:
|
||||
|
||||
@@ -444,7 +444,7 @@ class SellingController(StockController):
|
||||
get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")
|
||||
):
|
||||
# Get incoming rate based on original item cost based on valuation method
|
||||
qty = flt(d.get("stock_qty") or d.get("actual_qty"))
|
||||
qty = flt(d.get("stock_qty") or d.get("actual_qty") or d.get("qty"))
|
||||
|
||||
if (
|
||||
not d.incoming_rate
|
||||
|
||||
@@ -166,7 +166,7 @@ class StockController(AccountsController):
|
||||
# remove extra whitespace and store one serial no on each line
|
||||
row.serial_no = clean_serial_no_string(row.serial_no)
|
||||
|
||||
def make_bundle_using_old_serial_batch_fields(self, table_name=None):
|
||||
def make_bundle_using_old_serial_batch_fields(self, table_name=None, via_landed_cost_voucher=False):
|
||||
if self.get("_action") == "update_after_submit":
|
||||
return
|
||||
|
||||
@@ -205,7 +205,7 @@ class StockController(AccountsController):
|
||||
"company": self.company,
|
||||
"is_rejected": 1 if row.get("rejected_warehouse") else 0,
|
||||
"use_serial_batch_fields": row.use_serial_batch_fields,
|
||||
"do_not_submit": True,
|
||||
"do_not_submit": True if not via_landed_cost_voucher else False,
|
||||
}
|
||||
|
||||
if row.get("qty") or row.get("consumed_qty"):
|
||||
@@ -1119,7 +1119,7 @@ class StockController(AccountsController):
|
||||
message += _("Please adjust the qty or edit {0} to proceed.").format(rule_link)
|
||||
return message
|
||||
|
||||
def repost_future_sle_and_gle(self, force=False):
|
||||
def repost_future_sle_and_gle(self, force=False, via_landed_cost_voucher=False):
|
||||
args = frappe._dict(
|
||||
{
|
||||
"posting_date": self.posting_date,
|
||||
@@ -1127,6 +1127,7 @@ class StockController(AccountsController):
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"company": self.company,
|
||||
"via_landed_cost_voucher": via_landed_cost_voucher,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1138,7 +1139,11 @@ class StockController(AccountsController):
|
||||
frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting")
|
||||
)
|
||||
if item_based_reposting:
|
||||
create_item_wise_repost_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
create_item_wise_repost_entries(
|
||||
voucher_type=self.doctype,
|
||||
voucher_no=self.name,
|
||||
via_landed_cost_voucher=via_landed_cost_voucher,
|
||||
)
|
||||
else:
|
||||
create_repost_item_valuation_entry(args)
|
||||
|
||||
@@ -1510,11 +1515,14 @@ def create_repost_item_valuation_entry(args):
|
||||
repost_entry.allow_zero_rate = args.allow_zero_rate
|
||||
repost_entry.flags.ignore_links = True
|
||||
repost_entry.flags.ignore_permissions = True
|
||||
repost_entry.via_landed_cost_voucher = args.via_landed_cost_voucher
|
||||
repost_entry.save()
|
||||
repost_entry.submit()
|
||||
|
||||
|
||||
def create_item_wise_repost_entries(voucher_type, voucher_no, allow_zero_rate=False):
|
||||
def create_item_wise_repost_entries(
|
||||
voucher_type, voucher_no, allow_zero_rate=False, via_landed_cost_voucher=False
|
||||
):
|
||||
"""Using a voucher create repost item valuation records for all item-warehouse pairs."""
|
||||
|
||||
stock_ledger_entries = get_items_to_be_repost(voucher_type, voucher_no)
|
||||
@@ -1538,6 +1546,7 @@ def create_item_wise_repost_entries(voucher_type, voucher_no, allow_zero_rate=Fa
|
||||
repost_entry.allow_zero_rate = allow_zero_rate
|
||||
repost_entry.flags.ignore_links = True
|
||||
repost_entry.flags.ignore_permissions = True
|
||||
repost_entry.via_landed_cost_voucher = via_landed_cost_voucher
|
||||
repost_entry.submit()
|
||||
repost_entries.append(repost_entry)
|
||||
|
||||
|
||||
@@ -135,6 +135,27 @@ class TestAccountsController(FrappeTestCase):
|
||||
acc = frappe.get_doc("Account", name)
|
||||
self.debtors_usd = acc.name
|
||||
|
||||
account_name = "Creditors USD"
|
||||
if not frappe.db.get_value(
|
||||
"Account", filters={"account_name": account_name, "company": self.company}
|
||||
):
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.account_name = account_name
|
||||
acc.parent_account = "Accounts Payable - " + self.company_abbr
|
||||
acc.company = self.company
|
||||
acc.account_currency = "USD"
|
||||
acc.account_type = "Payable"
|
||||
acc.insert()
|
||||
else:
|
||||
name = frappe.db.get_value(
|
||||
"Account",
|
||||
filters={"account_name": account_name, "company": self.company},
|
||||
fieldname="name",
|
||||
pluck=True,
|
||||
)
|
||||
acc = frappe.get_doc("Account", name)
|
||||
self.creditors_usd = acc.name
|
||||
|
||||
def create_sales_invoice(
|
||||
self,
|
||||
qty=1,
|
||||
@@ -174,7 +195,9 @@ class TestAccountsController(FrappeTestCase):
|
||||
)
|
||||
return sinv
|
||||
|
||||
def create_payment_entry(self, amount=1, source_exc_rate=75, posting_date=None, customer=None):
|
||||
def create_payment_entry(
|
||||
self, amount=1, source_exc_rate=75, posting_date=None, customer=None, submit=True
|
||||
):
|
||||
"""
|
||||
Helper function to populate default values in payment entry
|
||||
"""
|
||||
@@ -1606,3 +1629,72 @@ class TestAccountsController(FrappeTestCase):
|
||||
exc_je_for_je2 = self.get_journals_for(je2.doctype, je2.name)
|
||||
self.assertEqual(exc_je_for_je1, [])
|
||||
self.assertEqual(exc_je_for_je2, [])
|
||||
|
||||
def test_61_payment_entry_against_journal_for_payable_accounts(self):
|
||||
# Invoices
|
||||
exc_rate1 = 75
|
||||
exc_rate2 = 77
|
||||
amount = 1
|
||||
je1 = self.create_journal_entry(
|
||||
acc1=self.creditors_usd,
|
||||
acc1_exc_rate=exc_rate1,
|
||||
acc2=self.cash,
|
||||
acc1_amount=-amount,
|
||||
acc2_amount=(-amount * 75),
|
||||
acc2_exc_rate=1,
|
||||
)
|
||||
je1.accounts[0].party_type = "Supplier"
|
||||
je1.accounts[0].party = self.supplier
|
||||
je1 = je1.save().submit()
|
||||
|
||||
# Payment
|
||||
pe = create_payment_entry(
|
||||
company=self.company,
|
||||
payment_type="Pay",
|
||||
party_type="Supplier",
|
||||
party=self.supplier,
|
||||
paid_from=self.cash,
|
||||
paid_to=self.creditors_usd,
|
||||
paid_amount=amount,
|
||||
)
|
||||
pe.target_exchange_rate = exc_rate2
|
||||
pe.received_amount = amount
|
||||
pe.paid_amount = amount * exc_rate2
|
||||
pe.save().submit()
|
||||
|
||||
pr = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Reconciliation",
|
||||
"company": self.company,
|
||||
"party_type": "Supplier",
|
||||
"party": self.supplier,
|
||||
"receivable_payable_account": get_party_account("Supplier", self.supplier, self.company),
|
||||
}
|
||||
)
|
||||
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [x.as_dict() for x in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
self.assertEqual(len(pr.invoices), 0)
|
||||
self.assertEqual(len(pr.payments), 0)
|
||||
|
||||
# There should be no outstanding in both currencies
|
||||
self.assert_ledger_outstanding(je1.doctype, je1.name, 0.0, 0.0)
|
||||
|
||||
# Exchange Gain/Loss Journal should've been created
|
||||
exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name)
|
||||
self.assertEqual(len(exc_je_for_je1), 1)
|
||||
|
||||
# Cancel Payment
|
||||
pe.reload()
|
||||
pe.cancel()
|
||||
|
||||
self.assert_ledger_outstanding(je1.doctype, je1.name, (amount * exc_rate1), amount)
|
||||
|
||||
# Exchange Gain/Loss Journal should've been cancelled
|
||||
exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name)
|
||||
self.assertEqual(exc_je_for_je1, [])
|
||||
|
||||
@@ -540,6 +540,7 @@ accounting_dimension_doctypes = [
|
||||
"Supplier Quotation Item",
|
||||
"Payment Reconciliation",
|
||||
"Payment Reconciliation Allocation",
|
||||
"Payment Request",
|
||||
]
|
||||
|
||||
get_matching_queries = (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -355,6 +355,7 @@ erpnext.patches.v14_0.update_total_asset_cost_field
|
||||
erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
|
||||
erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes
|
||||
erpnext.patches.v14_0.update_flag_for_return_invoices #2024-03-22
|
||||
erpnext.patches.v15_0.create_accounting_dimensions_in_payment_request
|
||||
# below migration patch should always run last
|
||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
create_accounting_dimensions_for_doctype,
|
||||
)
|
||||
|
||||
|
||||
def execute():
|
||||
create_accounting_dimensions_for_doctype(doctype="Payment Request")
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -384,7 +384,6 @@ def _make_sales_order(source_name, target_doc=None, customer_group=None, ignore_
|
||||
)
|
||||
|
||||
target.flags.ignore_permissions = ignore_permissions
|
||||
target.delivery_date = nowdate()
|
||||
target.run_method("set_missing_values")
|
||||
target.run_method("calculate_taxes_and_totals")
|
||||
|
||||
|
||||
@@ -122,6 +122,7 @@ class TestQuotation(FrappeTestCase):
|
||||
|
||||
sales_order.naming_series = "_T-Quotation-"
|
||||
sales_order.transaction_date = nowdate()
|
||||
sales_order.delivery_date = nowdate()
|
||||
sales_order.insert()
|
||||
|
||||
def test_make_sales_order_with_terms(self):
|
||||
@@ -152,6 +153,7 @@ class TestQuotation(FrappeTestCase):
|
||||
|
||||
sales_order.naming_series = "_T-Quotation-"
|
||||
sales_order.transaction_date = nowdate()
|
||||
sales_order.delivery_date = nowdate()
|
||||
sales_order.insert()
|
||||
|
||||
# Remove any unknown taxes if applied
|
||||
|
||||
@@ -169,6 +169,27 @@ frappe.ui.form.on("Sales Order", {
|
||||
);
|
||||
},
|
||||
|
||||
// When multiple companies are set up. in case company name is changed set default company address
|
||||
company: function (frm) {
|
||||
if (frm.doc.company) {
|
||||
frappe.call({
|
||||
method: "erpnext.setup.doctype.company.company.get_default_company_address",
|
||||
args: {
|
||||
name: frm.doc.company,
|
||||
existing_address: frm.doc.company_address || "",
|
||||
},
|
||||
debounce: 2000,
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
frm.set_value("company_address", r.message);
|
||||
} else {
|
||||
frm.set_value("company_address", "");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onload: function (frm) {
|
||||
if (!frm.doc.transaction_date) {
|
||||
frm.set_value("transaction_date", frappe.datetime.get_today());
|
||||
@@ -288,6 +309,7 @@ frappe.ui.form.on("Sales Order", {
|
||||
label: __("Items to Reserve"),
|
||||
allow_bulk_edit: false,
|
||||
cannot_add_rows: true,
|
||||
cannot_delete_rows: true,
|
||||
data: [],
|
||||
fields: [
|
||||
{
|
||||
@@ -356,7 +378,7 @@ frappe.ui.form.on("Sales Order", {
|
||||
],
|
||||
primary_action_label: __("Reserve Stock"),
|
||||
primary_action: () => {
|
||||
var data = { items: dialog.fields_dict.items.grid.data };
|
||||
var data = { items: dialog.fields_dict.items.grid.get_selected_children() };
|
||||
|
||||
if (data.items && data.items.length > 0) {
|
||||
frappe.call({
|
||||
@@ -373,9 +395,11 @@ frappe.ui.form.on("Sales Order", {
|
||||
frm.reload_doc();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
dialog.hide();
|
||||
dialog.hide();
|
||||
} else {
|
||||
frappe.msgprint(__("Please select items to reserve."));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -390,6 +414,7 @@ frappe.ui.form.on("Sales Order", {
|
||||
|
||||
if (unreserved_qty > 0) {
|
||||
dialog.fields_dict.items.df.data.push({
|
||||
__checked: 1,
|
||||
sales_order_item: item.name,
|
||||
item_code: item.item_code,
|
||||
warehouse: item.warehouse,
|
||||
@@ -414,6 +439,7 @@ frappe.ui.form.on("Sales Order", {
|
||||
label: __("Reserved Stock"),
|
||||
allow_bulk_edit: false,
|
||||
cannot_add_rows: true,
|
||||
cannot_delete_rows: true,
|
||||
in_place_edit: true,
|
||||
data: [],
|
||||
fields: [
|
||||
@@ -457,7 +483,7 @@ frappe.ui.form.on("Sales Order", {
|
||||
],
|
||||
primary_action_label: __("Unreserve Stock"),
|
||||
primary_action: () => {
|
||||
var data = { sr_entries: dialog.fields_dict.sr_entries.grid.data };
|
||||
var data = { sr_entries: dialog.fields_dict.sr_entries.grid.get_selected_children() };
|
||||
|
||||
if (data.sr_entries && data.sr_entries.length > 0) {
|
||||
frappe.call({
|
||||
@@ -473,9 +499,11 @@ frappe.ui.form.on("Sales Order", {
|
||||
frm.reload_doc();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
dialog.hide();
|
||||
dialog.hide();
|
||||
} else {
|
||||
frappe.msgprint(__("Please select items to unreserve."));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -250,9 +250,10 @@ class LandedCostVoucher(Document):
|
||||
|
||||
# update stock & gl entries for submit state of PR
|
||||
doc.docstatus = 1
|
||||
doc.make_bundle_using_old_serial_batch_fields(via_landed_cost_voucher=True)
|
||||
doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True)
|
||||
doc.make_gl_entries()
|
||||
doc.repost_future_sle_and_gle()
|
||||
doc.repost_future_sle_and_gle(via_landed_cost_voucher=True)
|
||||
|
||||
def validate_asset_qty_and_status(self, receipt_document_type, receipt_document):
|
||||
for item in self.get("items"):
|
||||
|
||||
@@ -596,6 +596,356 @@ class TestLandedCostVoucher(FrappeTestCase):
|
||||
lcv.cancel()
|
||||
pr.cancel()
|
||||
|
||||
def test_landed_cost_voucher_with_serial_batch_for_legacy_pr(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
frappe.flags.ignore_serial_batch_bundle_validation = True
|
||||
frappe.flags.use_serial_and_batch_fields = True
|
||||
sn_item = "Test Landed Cost Voucher Serial NO for Legacy PR"
|
||||
batch_item = "Test Landed Cost Voucher Batch NO for Legacy PR"
|
||||
sn_item_doc = make_item(
|
||||
sn_item,
|
||||
{
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "SN-TLCVSNO-.####",
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
)
|
||||
|
||||
batch_item_doc = make_item(
|
||||
batch_item,
|
||||
{
|
||||
"has_batch_no": 1,
|
||||
"batch_number_series": "BATCH-TLCVSNO-.####",
|
||||
"create_new_batch": 1,
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
)
|
||||
|
||||
serial_nos = [
|
||||
"SN-TLCVSNO-0001",
|
||||
"SN-TLCVSNO-0002",
|
||||
"SN-TLCVSNO-0003",
|
||||
"SN-TLCVSNO-0004",
|
||||
"SN-TLCVSNO-0005",
|
||||
]
|
||||
|
||||
for sn in serial_nos:
|
||||
if not frappe.db.exists("Serial No", sn):
|
||||
sn_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Serial No",
|
||||
"item_code": sn_item,
|
||||
"serial_no": sn,
|
||||
}
|
||||
)
|
||||
sn_doc.insert()
|
||||
|
||||
if not frappe.db.exists("Batch", "BATCH-TLCVSNO-0001"):
|
||||
batch_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"item": batch_item,
|
||||
"batch_id": "BATCH-TLCVSNO-0001",
|
||||
}
|
||||
)
|
||||
batch_doc.insert()
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
company = frappe.db.get_value("Warehouse", warehouse, "company")
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
company=company,
|
||||
warehouse=warehouse,
|
||||
item_code=sn_item,
|
||||
qty=5,
|
||||
rate=100,
|
||||
uom=sn_item_doc.stock_uom,
|
||||
stock_uom=sn_item_doc.stock_uom,
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
pr.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": batch_item,
|
||||
"item_name": batch_item,
|
||||
"description": "Test Batch Item",
|
||||
"uom": batch_item_doc.stock_uom,
|
||||
"stock_uom": batch_item_doc.stock_uom,
|
||||
"qty": 5,
|
||||
"rate": 100,
|
||||
"warehouse": warehouse,
|
||||
},
|
||||
)
|
||||
|
||||
pr.submit()
|
||||
pr.reload()
|
||||
|
||||
for row in pr.items:
|
||||
self.assertEqual(row.valuation_rate, 100)
|
||||
self.assertFalse(row.serial_no)
|
||||
self.assertFalse(row.batch_no)
|
||||
self.assertFalse(row.serial_and_batch_bundle)
|
||||
|
||||
if row.item_code == sn_item:
|
||||
row.db_set("serial_no", ", ".join(serial_nos))
|
||||
else:
|
||||
row.db_set("batch_no", "BATCH-TLCVSNO-0001")
|
||||
|
||||
for sn in serial_nos:
|
||||
sn_doc = frappe.get_doc("Serial No", sn)
|
||||
sn_doc.db_set(
|
||||
{
|
||||
"warehouse": warehouse,
|
||||
"status": "Active",
|
||||
}
|
||||
)
|
||||
|
||||
batch_doc.db_set(
|
||||
{
|
||||
"batch_qty": 5,
|
||||
}
|
||||
)
|
||||
|
||||
frappe.flags.ignore_serial_batch_bundle_validation = False
|
||||
frappe.flags.use_serial_and_batch_fields = False
|
||||
|
||||
lcv = make_landed_cost_voucher(
|
||||
company=pr.company,
|
||||
receipt_document_type="Purchase Receipt",
|
||||
receipt_document=pr.name,
|
||||
charges=20,
|
||||
distribute_charges_based_on="Qty",
|
||||
do_not_save=True,
|
||||
)
|
||||
|
||||
lcv.get_items_from_purchase_receipts()
|
||||
lcv.save()
|
||||
lcv.submit()
|
||||
|
||||
pr.reload()
|
||||
|
||||
for row in pr.items:
|
||||
self.assertEqual(row.valuation_rate, 102)
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
self.assertEqual(
|
||||
row.valuation_rate,
|
||||
frappe.db.get_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "avg_rate"),
|
||||
)
|
||||
|
||||
lcv.cancel()
|
||||
pr.reload()
|
||||
|
||||
for row in pr.items:
|
||||
self.assertEqual(row.valuation_rate, 100)
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
self.assertEqual(
|
||||
row.valuation_rate,
|
||||
frappe.db.get_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "avg_rate"),
|
||||
)
|
||||
|
||||
def test_do_not_validate_landed_cost_voucher_with_serial_batch_for_legacy_pr(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import get_auto_batch_nos
|
||||
|
||||
frappe.flags.ignore_serial_batch_bundle_validation = True
|
||||
frappe.flags.use_serial_and_batch_fields = True
|
||||
sn_item = "Test Don't Validate Landed Cost Voucher Serial NO for Legacy PR"
|
||||
batch_item = "Test Don't Validate Landed Cost Voucher Batch NO for Legacy PR"
|
||||
sn_item_doc = make_item(
|
||||
sn_item,
|
||||
{
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "SN-TDVLCVSNO-.####",
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
)
|
||||
|
||||
batch_item_doc = make_item(
|
||||
batch_item,
|
||||
{
|
||||
"has_batch_no": 1,
|
||||
"batch_number_series": "BATCH-TDVLCVSNO-.####",
|
||||
"create_new_batch": 1,
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
)
|
||||
|
||||
serial_nos = [
|
||||
"SN-TDVLCVSNO-0001",
|
||||
"SN-TDVLCVSNO-0002",
|
||||
"SN-TDVLCVSNO-0003",
|
||||
"SN-TDVLCVSNO-0004",
|
||||
"SN-TDVLCVSNO-0005",
|
||||
]
|
||||
|
||||
for sn in serial_nos:
|
||||
if not frappe.db.exists("Serial No", sn):
|
||||
sn_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Serial No",
|
||||
"item_code": sn_item,
|
||||
"serial_no": sn,
|
||||
}
|
||||
)
|
||||
sn_doc.insert()
|
||||
|
||||
if not frappe.db.exists("Batch", "BATCH-TDVLCVSNO-0001"):
|
||||
batch_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"item": batch_item,
|
||||
"batch_id": "BATCH-TDVLCVSNO-0001",
|
||||
}
|
||||
)
|
||||
batch_doc.insert()
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
company = frappe.db.get_value("Warehouse", warehouse, "company")
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
company=company,
|
||||
warehouse=warehouse,
|
||||
item_code=sn_item,
|
||||
qty=5,
|
||||
rate=100,
|
||||
uom=sn_item_doc.stock_uom,
|
||||
stock_uom=sn_item_doc.stock_uom,
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
pr.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": batch_item,
|
||||
"item_name": batch_item,
|
||||
"description": "Test Batch Item",
|
||||
"uom": batch_item_doc.stock_uom,
|
||||
"stock_uom": batch_item_doc.stock_uom,
|
||||
"qty": 5,
|
||||
"rate": 100,
|
||||
"warehouse": warehouse,
|
||||
},
|
||||
)
|
||||
|
||||
pr.submit()
|
||||
pr.reload()
|
||||
|
||||
for sn in serial_nos:
|
||||
sn_doc = frappe.get_doc("Serial No", sn)
|
||||
sn_doc.db_set(
|
||||
{
|
||||
"warehouse": warehouse,
|
||||
"status": "Active",
|
||||
}
|
||||
)
|
||||
|
||||
batch_doc.db_set(
|
||||
{
|
||||
"batch_qty": 5,
|
||||
}
|
||||
)
|
||||
|
||||
for row in pr.items:
|
||||
if row.item_code == sn_item:
|
||||
row.db_set("serial_no", ", ".join(serial_nos))
|
||||
else:
|
||||
row.db_set("batch_no", "BATCH-TDVLCVSNO-0001")
|
||||
|
||||
stock_ledger_entries = frappe.get_all("Stock Ledger Entry", filters={"voucher_no": pr.name})
|
||||
for sle in stock_ledger_entries:
|
||||
doc = frappe.get_doc("Stock Ledger Entry", sle.name)
|
||||
if doc.item_code == sn_item:
|
||||
doc.db_set("serial_no", ", ".join(serial_nos))
|
||||
else:
|
||||
doc.db_set("batch_no", "BATCH-TDVLCVSNO-0001")
|
||||
|
||||
dn = create_delivery_note(
|
||||
company=company,
|
||||
warehouse=warehouse,
|
||||
item_code=sn_item,
|
||||
qty=5,
|
||||
rate=100,
|
||||
uom=sn_item_doc.stock_uom,
|
||||
stock_uom=sn_item_doc.stock_uom,
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
dn.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": batch_item,
|
||||
"item_name": batch_item,
|
||||
"description": "Test Batch Item",
|
||||
"uom": batch_item_doc.stock_uom,
|
||||
"stock_uom": batch_item_doc.stock_uom,
|
||||
"qty": 5,
|
||||
"rate": 100,
|
||||
"warehouse": warehouse,
|
||||
},
|
||||
)
|
||||
|
||||
dn.submit()
|
||||
|
||||
stock_ledger_entries = frappe.get_all("Stock Ledger Entry", filters={"voucher_no": dn.name})
|
||||
for sle in stock_ledger_entries:
|
||||
doc = frappe.get_doc("Stock Ledger Entry", sle.name)
|
||||
if doc.item_code == sn_item:
|
||||
doc.db_set("serial_no", ", ".join(serial_nos))
|
||||
else:
|
||||
doc.db_set("batch_no", "BATCH-TDVLCVSNO-0001")
|
||||
|
||||
available_batches = get_auto_batch_nos(
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": batch_item,
|
||||
"warehouse": warehouse,
|
||||
"batch_no": ["BATCH-TDVLCVSNO-0001"],
|
||||
"consider_negative_batches": True,
|
||||
}
|
||||
)
|
||||
)[0]
|
||||
|
||||
self.assertFalse(available_batches.get("qty"))
|
||||
|
||||
frappe.flags.ignore_serial_batch_bundle_validation = False
|
||||
frappe.flags.use_serial_and_batch_fields = False
|
||||
|
||||
lcv = make_landed_cost_voucher(
|
||||
company=pr.company,
|
||||
receipt_document_type="Purchase Receipt",
|
||||
receipt_document=pr.name,
|
||||
charges=20,
|
||||
distribute_charges_based_on="Qty",
|
||||
do_not_save=True,
|
||||
)
|
||||
|
||||
lcv.get_items_from_purchase_receipts()
|
||||
lcv.save()
|
||||
lcv.submit()
|
||||
|
||||
pr.reload()
|
||||
|
||||
for row in pr.items:
|
||||
self.assertEqual(row.valuation_rate, 102)
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
self.assertEqual(
|
||||
row.valuation_rate,
|
||||
frappe.db.get_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "avg_rate"),
|
||||
)
|
||||
|
||||
lcv.cancel()
|
||||
pr.reload()
|
||||
|
||||
for row in pr.items:
|
||||
self.assertEqual(row.valuation_rate, 100)
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
self.assertEqual(
|
||||
row.valuation_rate,
|
||||
frappe.db.get_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "avg_rate"),
|
||||
)
|
||||
|
||||
|
||||
def make_landed_cost_voucher(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -860,6 +872,12 @@ class SerialandBatchBundle(Document):
|
||||
self.validate_batch_inventory()
|
||||
|
||||
def validate_batch_inventory(self):
|
||||
if (
|
||||
self.voucher_type in ["Purchase Invoice", "Purchase Receipt"]
|
||||
and frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus") == 1
|
||||
):
|
||||
return
|
||||
|
||||
if not self.has_batch_no:
|
||||
return
|
||||
|
||||
|
||||
@@ -9,7 +9,17 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate
|
||||
from frappe.utils import (
|
||||
cint,
|
||||
comma_or,
|
||||
cstr,
|
||||
flt,
|
||||
format_time,
|
||||
formatdate,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
nowdate,
|
||||
)
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.general_ledger import process_gl_map
|
||||
@@ -184,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()
|
||||
@@ -640,8 +651,8 @@ class StockEntry(StockController):
|
||||
)
|
||||
)
|
||||
|
||||
work_order_link = frappe.utils.get_link_to_form("Work Order", self.work_order)
|
||||
job_card_link = frappe.utils.get_link_to_form("Job Card", job_card)
|
||||
work_order_link = get_link_to_form("Work Order", self.work_order)
|
||||
job_card_link = get_link_to_form("Job Card", job_card)
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Operation {1} is not completed for {2} qty of finished goods in Work Order {3}. Please update operation status via Job Card {4}."
|
||||
@@ -1350,9 +1361,24 @@ class StockEntry(StockController):
|
||||
|
||||
return finished_item_row
|
||||
|
||||
def validate_serial_batch_bundle_type(self, serial_and_batch_bundle):
|
||||
if (
|
||||
frappe.db.get_value("Serial and Batch Bundle", serial_and_batch_bundle, "type_of_transaction")
|
||||
!= "Outward"
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"The Serial and Batch Bundle {0} is not valid for this transaction. The 'Type of Transaction' should be 'Outward' instead of 'Inward' in Serial and Batch Bundle {0}"
|
||||
).format(get_link_to_form("Serial and Batch Bundle", serial_and_batch_bundle)),
|
||||
title=_("Invalid Serial and Batch Bundle"),
|
||||
)
|
||||
|
||||
def get_sle_for_source_warehouse(self, sl_entries, finished_item_row):
|
||||
for d in self.get("items"):
|
||||
if cstr(d.s_warehouse):
|
||||
if d.serial_and_batch_bundle and self.docstatus == 1:
|
||||
self.validate_serial_batch_bundle_type(d.serial_and_batch_bundle)
|
||||
|
||||
sle = self.get_sl_entries(
|
||||
d,
|
||||
{
|
||||
@@ -1369,6 +1395,21 @@ class StockEntry(StockController):
|
||||
):
|
||||
sle.dependant_sle_voucher_detail_no = finished_item_row.name
|
||||
|
||||
if sle.serial_and_batch_bundle and self.docstatus == 2:
|
||||
bundle_id = frappe.get_cached_value(
|
||||
"Serial and Batch Bundle",
|
||||
{
|
||||
"voucher_detail_no": d.name,
|
||||
"voucher_no": self.name,
|
||||
"is_cancelled": 0,
|
||||
"type_of_transaction": "Outward",
|
||||
},
|
||||
"name",
|
||||
)
|
||||
|
||||
if bundle_id:
|
||||
sle.serial_and_batch_bundle = bundle_id
|
||||
|
||||
sl_entries.append(sle)
|
||||
|
||||
def make_serial_and_batch_bundle_for_transfer(self):
|
||||
@@ -1601,11 +1642,7 @@ class StockEntry(StockController):
|
||||
ret.update(get_uom_details(args.get("item_code"), args.get("uom"), args.get("qty")))
|
||||
|
||||
if self.purpose == "Material Issue":
|
||||
ret["expense_account"] = (
|
||||
item.get("expense_account")
|
||||
or item_group_defaults.get("expense_account")
|
||||
or frappe.get_cached_value("Company", self.company, "default_expense_account")
|
||||
)
|
||||
ret["expense_account"] = item.get("expense_account") or item_group_defaults.get("expense_account")
|
||||
|
||||
for company_field, field in {
|
||||
"stock_adjustment_account": "expense_account",
|
||||
|
||||
@@ -1745,6 +1745,41 @@ class TestStockEntry(FrappeTestCase):
|
||||
self.assertTrue(frappe.db.exists("Serial No", serial_no))
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Delivered")
|
||||
|
||||
def test_serial_batch_bundle_type_of_transaction(self):
|
||||
item = make_item(
|
||||
"Test Use Serial and Batch Item SN Item",
|
||||
{
|
||||
"has_batch_no": 1,
|
||||
"is_stock_item": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_naming_series": "Test-SBBTYT-NNS.#####",
|
||||
},
|
||||
).name
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=item,
|
||||
qty=2,
|
||||
target="_Test Warehouse - _TC",
|
||||
use_serial_batch_fields=1,
|
||||
)
|
||||
|
||||
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=item,
|
||||
qty=2,
|
||||
source="_Test Warehouse - _TC",
|
||||
target="Stores - _TC",
|
||||
use_serial_batch_fields=0,
|
||||
batch_no=batch_no,
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
se.reload()
|
||||
sbb = se.items[0].serial_and_batch_bundle
|
||||
frappe.db.set_value("Serial and Batch Bundle", sbb, "type_of_transaction", "Inward")
|
||||
self.assertRaises(frappe.ValidationError, se.submit)
|
||||
|
||||
|
||||
def make_serialized_item(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -86,7 +86,11 @@ class DelayedItemReport:
|
||||
filters = {"parent": ("in", sales_orders), "name": ("in", sales_order_items)}
|
||||
|
||||
so_data = {}
|
||||
for d in frappe.get_all(doctype, filters=filters, fields=["delivery_date", "parent", "name"]):
|
||||
fields = ["delivery_date", "name"]
|
||||
if frappe.db.has_column(doctype, "parent"):
|
||||
fields.append("parent")
|
||||
|
||||
for d in frappe.get_all(doctype, filters=filters, fields=fields):
|
||||
key = d.name if consolidated else (d.parent, d.name)
|
||||
if key not in so_data:
|
||||
so_data.setdefault(key, d.delivery_date)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -498,9 +498,6 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
return process_gl_map(gl_entries)
|
||||
|
||||
def make_item_gl_entries(self, gl_entries, warehouse_account=None):
|
||||
stock_rbnb = self.get_company_default("stock_received_but_not_billed")
|
||||
expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
|
||||
|
||||
warehouse_with_no_account = []
|
||||
|
||||
for item in self.items:
|
||||
@@ -518,31 +515,41 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
"stock_value_difference",
|
||||
)
|
||||
|
||||
warehouse_account_name = warehouse_account[item.warehouse]["account"]
|
||||
warehouse_account_currency = warehouse_account[item.warehouse]["account_currency"]
|
||||
accepted_warehouse_account = warehouse_account[item.warehouse]["account"]
|
||||
supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get(
|
||||
"account"
|
||||
)
|
||||
supplier_warehouse_account_currency = warehouse_account.get(
|
||||
self.supplier_warehouse, {}
|
||||
).get("account_currency")
|
||||
remarks = self.get("remarks") or _("Accounting Entry for Stock")
|
||||
|
||||
# FG Warehouse Account (Debit)
|
||||
# Accepted Warehouse Account (Debit)
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=warehouse_account_name,
|
||||
account=accepted_warehouse_account,
|
||||
cost_center=item.cost_center,
|
||||
debit=stock_value_diff,
|
||||
credit=0.0,
|
||||
remarks=remarks,
|
||||
against_account=stock_rbnb,
|
||||
account_currency=warehouse_account_currency,
|
||||
against_account=item.expense_account,
|
||||
account_currency=get_account_currency(accepted_warehouse_account),
|
||||
project=item.project,
|
||||
item=item,
|
||||
)
|
||||
# Expense Account (Credit)
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=item.expense_account,
|
||||
cost_center=item.cost_center,
|
||||
debit=0.0,
|
||||
credit=stock_value_diff,
|
||||
remarks=remarks,
|
||||
against_account=accepted_warehouse_account,
|
||||
account_currency=get_account_currency(item.expense_account),
|
||||
project=item.project,
|
||||
item=item,
|
||||
)
|
||||
|
||||
# Supplier Warehouse Account (Credit)
|
||||
if flt(item.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse):
|
||||
if flt(item.rm_supp_cost) and supplier_warehouse_account:
|
||||
# Supplier Warehouse Account (Credit)
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=supplier_warehouse_account,
|
||||
@@ -550,43 +557,66 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
debit=0.0,
|
||||
credit=flt(item.rm_supp_cost),
|
||||
remarks=remarks,
|
||||
against_account=warehouse_account_name,
|
||||
account_currency=supplier_warehouse_account_currency,
|
||||
against_account=item.expense_account,
|
||||
account_currency=get_account_currency(supplier_warehouse_account),
|
||||
project=item.project,
|
||||
item=item,
|
||||
)
|
||||
|
||||
# Expense Account (Credit)
|
||||
if flt(item.service_cost_per_qty):
|
||||
# Expense Account (Debit)
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=item.expense_account,
|
||||
cost_center=item.cost_center,
|
||||
debit=0.0,
|
||||
credit=flt(item.service_cost_per_qty) * flt(item.qty),
|
||||
debit=flt(item.rm_supp_cost),
|
||||
credit=0.0,
|
||||
remarks=remarks,
|
||||
against_account=warehouse_account_name,
|
||||
against_account=supplier_warehouse_account,
|
||||
account_currency=get_account_currency(item.expense_account),
|
||||
project=item.project,
|
||||
item=item,
|
||||
)
|
||||
|
||||
# Loss Account (Credit)
|
||||
divisional_loss = flt(item.amount - stock_value_diff, item.precision("amount"))
|
||||
# Expense Account (Debit)
|
||||
if item.additional_cost_per_qty:
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=item.expense_account,
|
||||
cost_center=self.cost_center or self.get_company_default("cost_center"),
|
||||
debit=item.qty * item.additional_cost_per_qty,
|
||||
credit=0.0,
|
||||
remarks=remarks,
|
||||
against_account=None,
|
||||
account_currency=get_account_currency(item.expense_account),
|
||||
)
|
||||
|
||||
if divisional_loss:
|
||||
if self.is_return:
|
||||
loss_account = expenses_included_in_valuation
|
||||
else:
|
||||
loss_account = item.expense_account
|
||||
if divisional_loss := flt(item.amount - stock_value_diff, item.precision("amount")):
|
||||
loss_account = self.get_company_default(
|
||||
"stock_adjustment_account", ignore_validation=True
|
||||
)
|
||||
|
||||
# Loss Account (Credit)
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=loss_account,
|
||||
cost_center=item.cost_center,
|
||||
debit=0.0,
|
||||
credit=divisional_loss,
|
||||
remarks=remarks,
|
||||
against_account=item.expense_account,
|
||||
account_currency=get_account_currency(loss_account),
|
||||
project=item.project,
|
||||
item=item,
|
||||
)
|
||||
# Expense Account (Debit)
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=item.expense_account,
|
||||
cost_center=item.cost_center,
|
||||
debit=divisional_loss,
|
||||
credit=0.0,
|
||||
remarks=remarks,
|
||||
against_account=warehouse_account_name,
|
||||
account_currency=get_account_currency(loss_account),
|
||||
against_account=loss_account,
|
||||
account_currency=get_account_currency(item.expense_account),
|
||||
project=item.project,
|
||||
item=item,
|
||||
)
|
||||
@@ -596,7 +626,6 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
):
|
||||
warehouse_with_no_account.append(item.warehouse)
|
||||
|
||||
# Additional Costs Expense Accounts (Credit)
|
||||
for row in self.additional_costs:
|
||||
credit_amount = (
|
||||
flt(row.base_amount)
|
||||
@@ -604,6 +633,7 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
else flt(row.amount)
|
||||
)
|
||||
|
||||
# Additional Cost Expense Account (Credit)
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=row.expense_account,
|
||||
@@ -612,6 +642,7 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
credit=credit_amount,
|
||||
remarks=remarks,
|
||||
against_account=None,
|
||||
account_currency=get_account_currency(row.expense_account),
|
||||
)
|
||||
|
||||
if warehouse_with_no_account:
|
||||
@@ -691,6 +722,7 @@ def make_purchase_receipt(source_name, target_doc=None, save=False, submit=False
|
||||
"purchase_order": item.purchase_order,
|
||||
"purchase_order_item": item.purchase_order_item,
|
||||
"subcontracting_receipt_item": item.name,
|
||||
"project": po_item.project,
|
||||
}
|
||||
target_doc.append("items", item_row)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from frappe.utils import add_days, cint, flt, nowtime, today
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||
from erpnext.accounts.utils import get_company_default
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
get_rm_items,
|
||||
@@ -351,26 +352,15 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
||||
self.assertEqual(cint(erpnext.is_perpetual_inventory_enabled(scr.company)), 1)
|
||||
|
||||
gl_entries = get_gl_entries("Subcontracting Receipt", scr.name)
|
||||
|
||||
self.assertTrue(gl_entries)
|
||||
|
||||
fg_warehouse_ac = get_inventory_account(scr.company, scr.items[0].warehouse)
|
||||
supplier_warehouse_ac = get_inventory_account(scr.company, scr.supplier_warehouse)
|
||||
expense_account = scr.items[0].expense_account
|
||||
|
||||
if fg_warehouse_ac == supplier_warehouse_ac:
|
||||
expected_values = {
|
||||
fg_warehouse_ac: [2100.0, 1000.0], # FG Amount (D), RM Cost (C)
|
||||
expense_account: [0.0, 1000.0], # Service Cost (C)
|
||||
additional_costs_expense_account: [0.0, 100.0], # Additional Cost (C)
|
||||
}
|
||||
else:
|
||||
expected_values = {
|
||||
fg_warehouse_ac: [2100.0, 0.0], # FG Amount (D)
|
||||
supplier_warehouse_ac: [0.0, 1000.0], # RM Cost (C)
|
||||
expense_account: [0.0, 1000.0], # Service Cost (C)
|
||||
additional_costs_expense_account: [0.0, 100.0], # Additional Cost (C)
|
||||
}
|
||||
expected_values = {
|
||||
fg_warehouse_ac: [2100.0, 1000],
|
||||
expense_account: [1100, 2100],
|
||||
additional_costs_expense_account: [0.0, 100.0],
|
||||
}
|
||||
|
||||
for gle in gl_entries:
|
||||
self.assertEqual(expected_values[gle.account][0], gle.debit)
|
||||
@@ -381,6 +371,53 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
||||
self.assertTrue(get_gl_entries("Subcontracting Receipt", scr.name))
|
||||
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
|
||||
|
||||
@change_settings("Stock Settings", {"use_serial_batch_fields": 0})
|
||||
def test_subcontracting_receipt_with_zero_service_cost(self):
|
||||
warehouse = "Stores - TCP1"
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": warehouse,
|
||||
"item_code": "Subcontracted Service Item 7",
|
||||
"qty": 10,
|
||||
"rate": 0,
|
||||
"fg_item": "Subcontracted Item SA7",
|
||||
"fg_item_qty": 10,
|
||||
},
|
||||
]
|
||||
sco = get_subcontracting_order(
|
||||
company="_Test Company with perpetual inventory",
|
||||
warehouse=warehouse,
|
||||
supplier_warehouse="Work In Progress - TCP1",
|
||||
service_items=service_items,
|
||||
)
|
||||
rm_items = get_rm_items(sco.supplied_items)
|
||||
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||
make_stock_transfer_entry(
|
||||
sco_no=sco.name,
|
||||
rm_items=rm_items,
|
||||
itemwise_details=copy.deepcopy(itemwise_details),
|
||||
)
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.save()
|
||||
scr.submit()
|
||||
|
||||
gl_entries = get_gl_entries("Subcontracting Receipt", scr.name)
|
||||
self.assertTrue(gl_entries)
|
||||
|
||||
fg_warehouse_ac = get_inventory_account(scr.company, scr.items[0].warehouse)
|
||||
expense_account = scr.items[0].expense_account
|
||||
expected_values = {
|
||||
fg_warehouse_ac: [1000, 1000],
|
||||
expense_account: [1000, 1000],
|
||||
}
|
||||
|
||||
for gle in gl_entries:
|
||||
self.assertEqual(expected_values[gle.account][0], gle.debit)
|
||||
self.assertEqual(expected_values[gle.account][1], gle.credit)
|
||||
|
||||
scr.reload()
|
||||
scr.cancel()
|
||||
|
||||
def test_supplied_items_consumed_qty(self):
|
||||
# Set Backflush Based On as "Material Transferred for Subcontracting" to transfer RM's more than the required qty
|
||||
set_backflush_based_on("Material Transferred for Subcontract")
|
||||
|
||||
@@ -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)),
|
||||
|
||||
Reference in New Issue
Block a user