mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-03 04:09:11 +00:00
Merge branch 'develop' of https://github.com/frappe/erpnext into multi_pr_pi
This commit is contained in:
@@ -54,6 +54,7 @@ class BankAccount(Document):
|
|||||||
self.validate_company()
|
self.validate_company()
|
||||||
self.validate_iban()
|
self.validate_iban()
|
||||||
self.validate_account()
|
self.validate_account()
|
||||||
|
self.update_default_bank_account()
|
||||||
|
|
||||||
def validate_account(self):
|
def validate_account(self):
|
||||||
if self.account:
|
if self.account:
|
||||||
@@ -100,23 +101,51 @@ class BankAccount(Document):
|
|||||||
if to_check % 97 != 1:
|
if to_check % 97 != 1:
|
||||||
frappe.throw(_("IBAN is not valid"))
|
frappe.throw(_("IBAN is not valid"))
|
||||||
|
|
||||||
|
def update_default_bank_account(self):
|
||||||
|
if self.is_default and not self.disabled:
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Bank Account",
|
||||||
|
{
|
||||||
|
"party_type": self.party_type,
|
||||||
|
"party": self.party,
|
||||||
|
"is_company_account": self.is_company_account,
|
||||||
|
"is_default": 1,
|
||||||
|
"disabled": 0,
|
||||||
|
},
|
||||||
|
"is_default",
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_bank_account(doctype, docname):
|
def make_bank_account(doctype, docname):
|
||||||
doc = frappe.new_doc("Bank Account")
|
doc = frappe.new_doc("Bank Account")
|
||||||
doc.party_type = doctype
|
doc.party_type = doctype
|
||||||
doc.party = docname
|
doc.party = docname
|
||||||
doc.is_default = 1
|
|
||||||
|
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
|
|
||||||
def get_party_bank_account(party_type, party):
|
def get_party_bank_account(party_type, party):
|
||||||
return frappe.db.get_value(party_type, party, "default_bank_account")
|
return frappe.db.get_value(
|
||||||
|
"Bank Account",
|
||||||
|
{"party_type": party_type, "party": party, "is_default": 1, "disabled": 0},
|
||||||
|
"name",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_default_company_bank_account(company):
|
def get_default_company_bank_account(company, party_type, party):
|
||||||
return frappe.db.get_value("Bank Account", {"company": company, "is_company_account": 1, "is_default": 1})
|
default_company_bank_account = frappe.db.get_value(party_type, party, "default_bank_account")
|
||||||
|
if default_company_bank_account:
|
||||||
|
if company != frappe.get_cached_value("Bank Account", default_company_bank_account, "company"):
|
||||||
|
default_company_bank_account = None
|
||||||
|
|
||||||
|
if not default_company_bank_account:
|
||||||
|
default_company_bank_account = frappe.db.get_value(
|
||||||
|
"Bank Account", {"company": company, "is_company_account": 1, "is_default": 1}
|
||||||
|
)
|
||||||
|
|
||||||
|
return default_company_bank_account
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
@@ -146,6 +146,10 @@ class Dunning(AccountsController):
|
|||||||
)
|
)
|
||||||
row.dunning_level = len(past_dunnings) + 1
|
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):
|
def resolve_dunning(doc, state):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ class GLEntry(Document):
|
|||||||
and self.company == dimension.company
|
and self.company == dimension.company
|
||||||
and dimension.mandatory_for_pl
|
and dimension.mandatory_for_pl
|
||||||
and not dimension.disabled
|
and not dimension.disabled
|
||||||
|
and not self.is_cancelled
|
||||||
):
|
):
|
||||||
if not self.get(dimension.fieldname):
|
if not self.get(dimension.fieldname):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
@@ -195,6 +196,7 @@ class GLEntry(Document):
|
|||||||
and self.company == dimension.company
|
and self.company == dimension.company
|
||||||
and dimension.mandatory_for_bs
|
and dimension.mandatory_for_bs
|
||||||
and not dimension.disabled
|
and not dimension.disabled
|
||||||
|
and not self.is_cancelled
|
||||||
):
|
):
|
||||||
if not self.get(dimension.fieldname):
|
if not self.get(dimension.fieldname):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
|
|||||||
0
erpnext/accounts/doctype/ledger_health/__init__.py
Normal file
0
erpnext/accounts/doctype/ledger_health/__init__.py
Normal file
8
erpnext/accounts/doctype/ledger_health/ledger_health.js
Normal file
8
erpnext/accounts/doctype/ledger_health/ledger_health.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
// frappe.ui.form.on("Ledger Health", {
|
||||||
|
// refresh(frm) {
|
||||||
|
|
||||||
|
// },
|
||||||
|
// });
|
||||||
70
erpnext/accounts/doctype/ledger_health/ledger_health.json
Normal file
70
erpnext/accounts/doctype/ledger_health/ledger_health.json
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"autoname": "autoincrement",
|
||||||
|
"creation": "2024-03-26 17:01:47.443986",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"voucher_type",
|
||||||
|
"voucher_no",
|
||||||
|
"checked_on",
|
||||||
|
"debit_credit_mismatch",
|
||||||
|
"general_and_payment_ledger_mismatch"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "voucher_type",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Voucher Type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "voucher_no",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Voucher No"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "debit_credit_mismatch",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Debit-Credit mismatch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "checked_on",
|
||||||
|
"fieldtype": "Datetime",
|
||||||
|
"label": "Checked On"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "general_and_payment_ledger_mismatch",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "General and Payment Ledger mismatch"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"in_create": 1,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2024-04-09 11:16:07.044484",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Ledger Health",
|
||||||
|
"naming_rule": "Autoincrement",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"read_only": 1,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
25
erpnext/accounts/doctype/ledger_health/ledger_health.py
Normal file
25
erpnext/accounts/doctype/ledger_health/ledger_health.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class LedgerHealth(Document):
|
||||||
|
# begin: auto-generated types
|
||||||
|
# This code is auto-generated. Do not modify anything in this block.
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from frappe.types import DF
|
||||||
|
|
||||||
|
checked_on: DF.Datetime | None
|
||||||
|
debit_credit_mismatch: DF.Check
|
||||||
|
general_and_payment_ledger_mismatch: DF.Check
|
||||||
|
name: DF.Int | None
|
||||||
|
voucher_no: DF.Data | None
|
||||||
|
voucher_type: DF.Data | None
|
||||||
|
# end: auto-generated types
|
||||||
|
|
||||||
|
pass
|
||||||
109
erpnext/accounts/doctype/ledger_health/test_ledger_health.py
Normal file
109
erpnext/accounts/doctype/ledger_health/test_ledger_health.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import qb
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
from frappe.utils import nowdate
|
||||||
|
|
||||||
|
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||||
|
from erpnext.accounts.utils import run_ledger_health_checks
|
||||||
|
|
||||||
|
|
||||||
|
class TestLedgerHealth(AccountsTestMixin, FrappeTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.create_company()
|
||||||
|
self.create_customer()
|
||||||
|
self.configure_monitoring_tool()
|
||||||
|
self.clear_old_entries()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
def configure_monitoring_tool(self):
|
||||||
|
monitor_settings = frappe.get_doc("Ledger Health Monitor")
|
||||||
|
monitor_settings.enable_health_monitor = True
|
||||||
|
monitor_settings.enable_for_last_x_days = 60
|
||||||
|
monitor_settings.debit_credit_mismatch = True
|
||||||
|
monitor_settings.general_and_payment_ledger_mismatch = True
|
||||||
|
exists = [x for x in monitor_settings.companies if x.company == self.company]
|
||||||
|
if not exists:
|
||||||
|
monitor_settings.append("companies", {"company": self.company})
|
||||||
|
monitor_settings.save()
|
||||||
|
|
||||||
|
def clear_old_entries(self):
|
||||||
|
super().clear_old_entries()
|
||||||
|
lh = qb.DocType("Ledger Health")
|
||||||
|
qb.from_(lh).delete().run()
|
||||||
|
|
||||||
|
def create_journal(self):
|
||||||
|
je = frappe.new_doc("Journal Entry")
|
||||||
|
je.company = self.company
|
||||||
|
je.voucher_type = "Journal Entry"
|
||||||
|
je.posting_date = nowdate()
|
||||||
|
je.append(
|
||||||
|
"accounts",
|
||||||
|
{
|
||||||
|
"account": self.debit_to,
|
||||||
|
"party_type": "Customer",
|
||||||
|
"party": self.customer,
|
||||||
|
"debit_in_account_currency": 10000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
je.append("accounts", {"account": self.income_account, "credit_in_account_currency": 10000})
|
||||||
|
je.save().submit()
|
||||||
|
self.je = je
|
||||||
|
|
||||||
|
def test_debit_credit_mismatch(self):
|
||||||
|
self.create_journal()
|
||||||
|
|
||||||
|
# manually cause debit-credit mismatch
|
||||||
|
gle = frappe.db.get_all(
|
||||||
|
"GL Entry", filters={"voucher_no": self.je.name, "account": self.income_account}
|
||||||
|
)[0]
|
||||||
|
frappe.db.set_value("GL Entry", gle.name, "credit", 8000)
|
||||||
|
|
||||||
|
run_ledger_health_checks()
|
||||||
|
expected = {
|
||||||
|
"voucher_type": self.je.doctype,
|
||||||
|
"voucher_no": self.je.name,
|
||||||
|
"debit_credit_mismatch": True,
|
||||||
|
"general_and_payment_ledger_mismatch": False,
|
||||||
|
}
|
||||||
|
actual = frappe.db.get_all(
|
||||||
|
"Ledger Health",
|
||||||
|
fields=[
|
||||||
|
"voucher_type",
|
||||||
|
"voucher_no",
|
||||||
|
"debit_credit_mismatch",
|
||||||
|
"general_and_payment_ledger_mismatch",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.assertEqual(len(actual), 1)
|
||||||
|
self.assertEqual(expected, actual[0])
|
||||||
|
|
||||||
|
def test_gl_and_pl_mismatch(self):
|
||||||
|
self.create_journal()
|
||||||
|
|
||||||
|
# manually cause GL and PL discrepancy
|
||||||
|
ple = frappe.db.get_all("Payment Ledger Entry", filters={"voucher_no": self.je.name})[0]
|
||||||
|
frappe.db.set_value("Payment Ledger Entry", ple.name, "amount", 11000)
|
||||||
|
|
||||||
|
run_ledger_health_checks()
|
||||||
|
expected = {
|
||||||
|
"voucher_type": self.je.doctype,
|
||||||
|
"voucher_no": self.je.name,
|
||||||
|
"debit_credit_mismatch": False,
|
||||||
|
"general_and_payment_ledger_mismatch": True,
|
||||||
|
}
|
||||||
|
actual = frappe.db.get_all(
|
||||||
|
"Ledger Health",
|
||||||
|
fields=[
|
||||||
|
"voucher_type",
|
||||||
|
"voucher_no",
|
||||||
|
"debit_credit_mismatch",
|
||||||
|
"general_and_payment_ledger_mismatch",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.assertEqual(len(actual), 1)
|
||||||
|
self.assertEqual(expected, actual[0])
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
// frappe.ui.form.on("Ledger Health Monitor", {
|
||||||
|
// refresh(frm) {
|
||||||
|
|
||||||
|
// },
|
||||||
|
// });
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2024-03-27 09:38:07.427997",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"enable_health_monitor",
|
||||||
|
"monitor_section",
|
||||||
|
"monitor_for_last_x_days",
|
||||||
|
"debit_credit_mismatch",
|
||||||
|
"general_and_payment_ledger_mismatch",
|
||||||
|
"section_break_xdsp",
|
||||||
|
"companies"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "enable_health_monitor",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Enable Health Monitor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "monitor_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Configuration"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "debit_credit_mismatch",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Debit-Credit Mismatch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "general_and_payment_ledger_mismatch",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Discrepancy between General and Payment Ledger"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "60",
|
||||||
|
"fieldname": "monitor_for_last_x_days",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Monitor for Last 'X' days",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_xdsp",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Companies"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "companies",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"options": "Ledger Health Monitor Company"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hide_toolbar": 1,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"issingle": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2024-03-27 10:14:16.511681",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Ledger Health Monitor",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"role": "Accounts Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"role": "Accounts User",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class LedgerHealthMonitor(Document):
|
||||||
|
# begin: auto-generated types
|
||||||
|
# This code is auto-generated. Do not modify anything in this block.
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from frappe.types import DF
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.ledger_health_monitor_company.ledger_health_monitor_company import (
|
||||||
|
LedgerHealthMonitorCompany,
|
||||||
|
)
|
||||||
|
|
||||||
|
companies: DF.Table[LedgerHealthMonitorCompany]
|
||||||
|
debit_credit_mismatch: DF.Check
|
||||||
|
enable_health_monitor: DF.Check
|
||||||
|
general_and_payment_ledger_mismatch: DF.Check
|
||||||
|
monitor_for_last_x_days: DF.Int
|
||||||
|
# end: auto-generated types
|
||||||
|
|
||||||
|
pass
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestLedgerHealthMonitor(FrappeTestCase):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2024-03-27 10:04:45.727054",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"company"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "company",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Company",
|
||||||
|
"options": "Company"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2024-03-27 10:06:22.806155",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Ledger Health Monitor Company",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class LedgerHealthMonitorCompany(Document):
|
||||||
|
# begin: auto-generated types
|
||||||
|
# This code is auto-generated. Do not modify anything in this block.
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from frappe.types import DF
|
||||||
|
|
||||||
|
company: DF.Link | None
|
||||||
|
parent: DF.Data
|
||||||
|
parentfield: DF.Data
|
||||||
|
parenttype: DF.Data
|
||||||
|
# end: auto-generated types
|
||||||
|
|
||||||
|
pass
|
||||||
@@ -1665,6 +1665,8 @@ frappe.ui.form.on("Payment Entry Reference", {
|
|||||||
frm.doc.payment_type == "Receive"
|
frm.doc.payment_type == "Receive"
|
||||||
? frm.doc.paid_from_account_currency
|
? frm.doc.paid_from_account_currency
|
||||||
: frm.doc.paid_to_account_currency,
|
: frm.doc.paid_to_account_currency,
|
||||||
|
party_type: frm.doc.party_type,
|
||||||
|
party: frm.doc.party,
|
||||||
},
|
},
|
||||||
callback: function (r, rt) {
|
callback: function (r, rt) {
|
||||||
if (r.message) {
|
if (r.message) {
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ class PaymentEntry(AccountsController):
|
|||||||
self.update_outstanding_amounts()
|
self.update_outstanding_amounts()
|
||||||
self.update_advance_paid()
|
self.update_advance_paid()
|
||||||
self.update_payment_schedule()
|
self.update_payment_schedule()
|
||||||
|
self.set_payment_req_status()
|
||||||
self.set_status()
|
self.set_status()
|
||||||
|
|
||||||
def set_liability_account(self):
|
def set_liability_account(self):
|
||||||
@@ -484,7 +485,7 @@ class PaymentEntry(AccountsController):
|
|||||||
self,
|
self,
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
update_ref_details_only_for: list | None = None,
|
update_ref_details_only_for: list | None = None,
|
||||||
ref_exchange_rate: float | None = None,
|
reference_exchange_details: dict | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
for d in self.get("references"):
|
for d in self.get("references"):
|
||||||
if d.allocated_amount:
|
if d.allocated_amount:
|
||||||
@@ -495,12 +496,20 @@ class PaymentEntry(AccountsController):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
ref_details = get_reference_details(
|
ref_details = get_reference_details(
|
||||||
d.reference_doctype, d.reference_name, self.party_account_currency
|
d.reference_doctype,
|
||||||
|
d.reference_name,
|
||||||
|
self.party_account_currency,
|
||||||
|
self.party_type,
|
||||||
|
self.party,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Only update exchange rate when the reference is Journal Entry
|
# Only update exchange rate when the reference is Journal Entry
|
||||||
if ref_exchange_rate and d.reference_doctype == "Journal Entry":
|
if (
|
||||||
ref_details.update({"exchange_rate": ref_exchange_rate})
|
reference_exchange_details
|
||||||
|
and d.reference_doctype == reference_exchange_details.reference_doctype
|
||||||
|
and d.reference_name == reference_exchange_details.reference_name
|
||||||
|
):
|
||||||
|
ref_details.update({"exchange_rate": reference_exchange_details.exchange_rate})
|
||||||
|
|
||||||
for field, value in ref_details.items():
|
for field, value in ref_details.items():
|
||||||
if d.exchange_gain_loss:
|
if d.exchange_gain_loss:
|
||||||
@@ -1120,9 +1129,11 @@ class PaymentEntry(AccountsController):
|
|||||||
else:
|
else:
|
||||||
remarks = [
|
remarks = [
|
||||||
_("Amount {0} {1} {2} {3}").format(
|
_("Amount {0} {1} {2} {3}").format(
|
||||||
_(self.party_account_currency),
|
_(self.paid_to_account_currency)
|
||||||
|
if self.payment_type == "Receive"
|
||||||
|
else _(self.paid_from_account_currency),
|
||||||
self.paid_amount if self.payment_type == "Receive" else self.received_amount,
|
self.paid_amount if self.payment_type == "Receive" else self.received_amount,
|
||||||
_("received from") if self.payment_type == "Receive" else _("to"),
|
_("received from") if self.payment_type == "Receive" else _("paid to"),
|
||||||
self.party,
|
self.party,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@@ -1311,6 +1322,18 @@ class PaymentEntry(AccountsController):
|
|||||||
):
|
):
|
||||||
self.add_advance_gl_for_reference(gl_entries, ref)
|
self.add_advance_gl_for_reference(gl_entries, ref)
|
||||||
|
|
||||||
|
def get_dr_and_account_for_advances(self, reference):
|
||||||
|
if reference.reference_doctype == "Sales Invoice":
|
||||||
|
return "credit", reference.account
|
||||||
|
|
||||||
|
if reference.reference_doctype == "Payment Entry":
|
||||||
|
if reference.account_type == "Receivable" and reference.payment_type == "Pay":
|
||||||
|
return "credit", self.party_account
|
||||||
|
else:
|
||||||
|
return "debit", self.party_account
|
||||||
|
|
||||||
|
return "debit", reference.account
|
||||||
|
|
||||||
def add_advance_gl_for_reference(self, gl_entries, invoice):
|
def add_advance_gl_for_reference(self, gl_entries, invoice):
|
||||||
args_dict = {
|
args_dict = {
|
||||||
"party_type": self.party_type,
|
"party_type": self.party_type,
|
||||||
@@ -1330,8 +1353,8 @@ class PaymentEntry(AccountsController):
|
|||||||
if getdate(posting_date) < getdate(self.posting_date):
|
if getdate(posting_date) < getdate(self.posting_date):
|
||||||
posting_date = self.posting_date
|
posting_date = self.posting_date
|
||||||
|
|
||||||
dr_or_cr = "credit" if invoice.reference_doctype in ["Sales Invoice", "Payment Entry"] else "debit"
|
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
|
||||||
args_dict["account"] = invoice.account
|
args_dict["account"] = account
|
||||||
args_dict[dr_or_cr] = invoice.allocated_amount
|
args_dict[dr_or_cr] = invoice.allocated_amount
|
||||||
args_dict[dr_or_cr + "_in_account_currency"] = invoice.allocated_amount
|
args_dict[dr_or_cr + "_in_account_currency"] = invoice.allocated_amount
|
||||||
args_dict.update(
|
args_dict.update(
|
||||||
@@ -2115,8 +2138,7 @@ def get_party_details(company, party_type, party, date, cost_center=None):
|
|||||||
if party_type in ["Customer", "Supplier"]:
|
if party_type in ["Customer", "Supplier"]:
|
||||||
party_bank_account = get_party_bank_account(party_type, party)
|
party_bank_account = get_party_bank_account(party_type, party)
|
||||||
|
|
||||||
bank_account = get_default_company_bank_account(company)
|
bank_account = get_default_company_bank_account(company, party_type, party)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"party_account": party_account,
|
"party_account": party_account,
|
||||||
"party_name": party_name,
|
"party_name": party_name,
|
||||||
@@ -2157,49 +2179,75 @@ def get_company_defaults(company):
|
|||||||
return frappe.get_cached_value("Company", company, fields, as_dict=1)
|
return frappe.get_cached_value("Company", company, fields, as_dict=1)
|
||||||
|
|
||||||
|
|
||||||
def get_outstanding_on_journal_entry(name):
|
def get_outstanding_on_journal_entry(voucher_no, party_type, party):
|
||||||
gl = frappe.qb.DocType("GL Entry")
|
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||||
res = (
|
|
||||||
frappe.qb.from_(gl)
|
outstanding = (
|
||||||
.select(
|
frappe.qb.from_(ple)
|
||||||
Case()
|
.select(Sum(ple.amount_in_account_currency))
|
||||||
.when(
|
|
||||||
gl.party_type == "Customer",
|
|
||||||
Coalesce(Sum(gl.debit_in_account_currency - gl.credit_in_account_currency), 0),
|
|
||||||
)
|
|
||||||
.else_(Coalesce(Sum(gl.credit_in_account_currency - gl.debit_in_account_currency), 0))
|
|
||||||
.as_("outstanding_amount")
|
|
||||||
)
|
|
||||||
.where(
|
.where(
|
||||||
(Coalesce(gl.party_type, "") != "")
|
(ple.against_voucher_no == voucher_no)
|
||||||
& (gl.is_cancelled == 0)
|
& (ple.party_type == party_type)
|
||||||
& ((gl.voucher_no == name) | (gl.against_voucher == name))
|
& (ple.party == party)
|
||||||
|
& (ple.delinked == 0)
|
||||||
)
|
)
|
||||||
).run(as_dict=True)
|
).run()
|
||||||
|
|
||||||
outstanding_amount = res[0].get("outstanding_amount", 0) if res else 0
|
outstanding_amount = outstanding[0][0] if outstanding else 0
|
||||||
|
|
||||||
return outstanding_amount
|
total = (
|
||||||
|
frappe.qb.from_(ple)
|
||||||
|
.select(Sum(ple.amount_in_account_currency))
|
||||||
|
.where(
|
||||||
|
(ple.voucher_no == voucher_no)
|
||||||
|
& (ple.party_type == party_type)
|
||||||
|
& (ple.party == party)
|
||||||
|
& (ple.delinked == 0)
|
||||||
|
)
|
||||||
|
).run()
|
||||||
|
|
||||||
|
total_amount = total[0][0] if total else 0
|
||||||
|
|
||||||
|
return outstanding_amount, total_amount
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_reference_details(reference_doctype, reference_name, party_account_currency):
|
def get_reference_details(
|
||||||
|
reference_doctype, reference_name, party_account_currency, party_type=None, party=None
|
||||||
|
):
|
||||||
total_amount = outstanding_amount = exchange_rate = account = None
|
total_amount = outstanding_amount = exchange_rate = account = None
|
||||||
|
|
||||||
ref_doc = frappe.get_doc(reference_doctype, reference_name)
|
ref_doc = frappe.get_doc(reference_doctype, reference_name)
|
||||||
company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency(ref_doc.company)
|
company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency(ref_doc.company)
|
||||||
|
|
||||||
|
# Only applies for Reverse Payment Entries
|
||||||
|
account_type = None
|
||||||
|
payment_type = None
|
||||||
|
|
||||||
if reference_doctype == "Dunning":
|
if reference_doctype == "Dunning":
|
||||||
total_amount = outstanding_amount = ref_doc.get("dunning_amount")
|
total_amount = outstanding_amount = ref_doc.get("dunning_amount")
|
||||||
exchange_rate = 1
|
exchange_rate = 1
|
||||||
|
|
||||||
elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1:
|
elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1:
|
||||||
total_amount = ref_doc.get("total_amount")
|
|
||||||
if ref_doc.multi_currency:
|
if ref_doc.multi_currency:
|
||||||
exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
|
exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
|
||||||
else:
|
else:
|
||||||
exchange_rate = 1
|
exchange_rate = 1
|
||||||
outstanding_amount = get_outstanding_on_journal_entry(reference_name)
|
outstanding_amount, total_amount = get_outstanding_on_journal_entry(
|
||||||
|
reference_name, party_type, party
|
||||||
|
)
|
||||||
|
|
||||||
|
elif reference_doctype == "Payment Entry":
|
||||||
|
if reverse_payment_details := frappe.db.get_all(
|
||||||
|
"Payment Entry",
|
||||||
|
filters={"name": reference_name},
|
||||||
|
fields=["payment_type", "party_type"],
|
||||||
|
)[0]:
|
||||||
|
payment_type = reverse_payment_details.payment_type
|
||||||
|
account_type = frappe.db.get_value(
|
||||||
|
"Party Type", reverse_payment_details.party_type, "account_type"
|
||||||
|
)
|
||||||
|
exchange_rate = 1
|
||||||
|
|
||||||
elif reference_doctype != "Journal Entry":
|
elif reference_doctype != "Journal Entry":
|
||||||
if not total_amount:
|
if not total_amount:
|
||||||
@@ -2245,6 +2293,8 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
|||||||
"outstanding_amount": flt(outstanding_amount),
|
"outstanding_amount": flt(outstanding_amount),
|
||||||
"exchange_rate": flt(exchange_rate),
|
"exchange_rate": flt(exchange_rate),
|
||||||
"bill_no": ref_doc.get("bill_no"),
|
"bill_no": ref_doc.get("bill_no"),
|
||||||
|
"account_type": account_type,
|
||||||
|
"payment_type": payment_type,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if account:
|
if account:
|
||||||
|
|||||||
@@ -1074,9 +1074,13 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
pe.source_exchange_rate = 50
|
pe.source_exchange_rate = 50
|
||||||
pe.save()
|
pe.save()
|
||||||
|
|
||||||
ref_details = get_reference_details(so.doctype, so.name, pe.paid_from_account_currency)
|
ref_details = get_reference_details(
|
||||||
|
so.doctype, so.name, pe.paid_from_account_currency, "Customer", so.customer
|
||||||
|
)
|
||||||
expected_response = {
|
expected_response = {
|
||||||
"account": get_party_account("Customer", so.customer, so.company),
|
"account": get_party_account("Customer", so.customer, so.company),
|
||||||
|
"account_type": None, # only applies for Reverse Payment Entry
|
||||||
|
"payment_type": None, # only applies for Reverse Payment Entry
|
||||||
"total_amount": 5000.0,
|
"total_amount": 5000.0,
|
||||||
"outstanding_amount": 5000.0,
|
"outstanding_amount": 5000.0,
|
||||||
"exchange_rate": 1.0,
|
"exchange_rate": 1.0,
|
||||||
@@ -1543,7 +1547,7 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
company = "_Test Company"
|
company = "_Test Company"
|
||||||
customer = create_customer(frappe.generate_hash(length=10), "INR")
|
customer = create_customer(frappe.generate_hash(length=10), "INR")
|
||||||
advance_account = create_account(
|
advance_account = create_account(
|
||||||
parent_account="Current Assets - _TC",
|
parent_account="Current Liabilities - _TC",
|
||||||
account_name="Advances Received",
|
account_name="Advances Received",
|
||||||
company=company,
|
company=company,
|
||||||
account_type="Receivable",
|
account_type="Receivable",
|
||||||
@@ -1599,9 +1603,9 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
|
|
||||||
# assert General and Payment Ledger entries post partial reconciliation
|
# assert General and Payment Ledger entries post partial reconciliation
|
||||||
self.expected_gle = [
|
self.expected_gle = [
|
||||||
{"account": "Debtors - _TC", "debit": 0.0, "credit": 400.0},
|
|
||||||
{"account": advance_account, "debit": 400.0, "credit": 0.0},
|
{"account": advance_account, "debit": 400.0, "credit": 0.0},
|
||||||
{"account": advance_account, "debit": 0.0, "credit": 1000.0},
|
{"account": advance_account, "debit": 0.0, "credit": 1000.0},
|
||||||
|
{"account": advance_account, "debit": 0.0, "credit": 400.0},
|
||||||
{"account": "_Test Cash - _TC", "debit": 1000.0, "credit": 0.0},
|
{"account": "_Test Cash - _TC", "debit": 1000.0, "credit": 0.0},
|
||||||
]
|
]
|
||||||
self.expected_ple = [
|
self.expected_ple = [
|
||||||
@@ -1612,7 +1616,7 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
"amount": -1000.0,
|
"amount": -1000.0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"account": "Debtors - _TC",
|
"account": advance_account,
|
||||||
"voucher_no": pe.name,
|
"voucher_no": pe.name,
|
||||||
"against_voucher_no": reverse_pe.name,
|
"against_voucher_no": reverse_pe.name,
|
||||||
"amount": -400.0,
|
"amount": -400.0,
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
"due_date",
|
"due_date",
|
||||||
"bill_no",
|
"bill_no",
|
||||||
"payment_term",
|
"payment_term",
|
||||||
|
"account_type",
|
||||||
|
"payment_type",
|
||||||
"column_break_4",
|
"column_break_4",
|
||||||
"total_amount",
|
"total_amount",
|
||||||
"outstanding_amount",
|
"outstanding_amount",
|
||||||
@@ -108,12 +110,22 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Account",
|
"label": "Account",
|
||||||
"options": "Account"
|
"options": "Account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "account_type",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Account Type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "payment_type",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Payment Type"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:09.578312",
|
"modified": "2024-04-05 09:44:08.310593",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Entry Reference",
|
"name": "Payment Entry Reference",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class PaymentEntryReference(Document):
|
|||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
account: DF.Link | None
|
account: DF.Link | None
|
||||||
|
account_type: DF.Data | None
|
||||||
allocated_amount: DF.Float
|
allocated_amount: DF.Float
|
||||||
bill_no: DF.Data | None
|
bill_no: DF.Data | None
|
||||||
due_date: DF.Date | None
|
due_date: DF.Date | None
|
||||||
@@ -25,6 +26,7 @@ class PaymentEntryReference(Document):
|
|||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
payment_term: DF.Link | None
|
payment_term: DF.Link | None
|
||||||
|
payment_type: DF.Data | None
|
||||||
reference_doctype: DF.Link
|
reference_doctype: DF.Link
|
||||||
reference_name: DF.DynamicLink
|
reference_name: DF.DynamicLink
|
||||||
total_amount: DF.Float
|
total_amount: DF.Float
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import unittest
|
|||||||
|
|
||||||
# test_records = frappe.get_test_records('Payment Gateway Account')
|
# test_records = frappe.get_test_records('Payment Gateway Account')
|
||||||
|
|
||||||
|
test_ignore = ["Payment Gateway"]
|
||||||
|
|
||||||
|
|
||||||
class TestPaymentGatewayAccount(unittest.TestCase):
|
class TestPaymentGatewayAccount(unittest.TestCase):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -101,6 +101,14 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
"account_currency": "USD",
|
"account_currency": "USD",
|
||||||
"account_type": "Payable",
|
"account_type": "Payable",
|
||||||
},
|
},
|
||||||
|
# 'Payable' account for capturing advance paid, under 'Assets' group
|
||||||
|
{
|
||||||
|
"attribute": "advance_payable_account",
|
||||||
|
"account_name": "Advance Paid",
|
||||||
|
"parent_account": "Current Assets - _PR",
|
||||||
|
"account_currency": "INR",
|
||||||
|
"account_type": "Payable",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
for x in accounts:
|
for x in accounts:
|
||||||
@@ -1335,6 +1343,188 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
# Should not raise frappe.exceptions.ValidationError: Payment Entry has been modified after you pulled it. Please pull it again.
|
# Should not raise frappe.exceptions.ValidationError: Payment Entry has been modified after you pulled it. Please pull it again.
|
||||||
pr.reconcile()
|
pr.reconcile()
|
||||||
|
|
||||||
|
def test_reverse_payment_against_payment_for_supplier(self):
|
||||||
|
"""
|
||||||
|
Reconcile a payment against a reverse payment, for a supplier.
|
||||||
|
"""
|
||||||
|
self.supplier = "_Test Supplier"
|
||||||
|
amount = 4000
|
||||||
|
|
||||||
|
pe = self.create_payment_entry(amount=amount)
|
||||||
|
pe.party_type = "Supplier"
|
||||||
|
pe.party = self.supplier
|
||||||
|
pe.payment_type = "Pay"
|
||||||
|
pe.paid_from = self.cash
|
||||||
|
pe.paid_to = self.creditors
|
||||||
|
pe.save().submit()
|
||||||
|
|
||||||
|
reverse_pe = self.create_payment_entry(amount=amount)
|
||||||
|
reverse_pe.party_type = "Supplier"
|
||||||
|
reverse_pe.party = self.supplier
|
||||||
|
reverse_pe.payment_type = "Receive"
|
||||||
|
reverse_pe.paid_from = self.creditors
|
||||||
|
reverse_pe.paid_to = self.cash
|
||||||
|
reverse_pe.save().submit()
|
||||||
|
|
||||||
|
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
self.assertEqual(len(pr.invoices), 1)
|
||||||
|
self.assertEqual(len(pr.payments), 1)
|
||||||
|
self.assertEqual(pr.invoices[0].invoice_number, reverse_pe.name)
|
||||||
|
self.assertEqual(pr.payments[0].reference_name, pe.name)
|
||||||
|
|
||||||
|
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||||
|
payments = [payment.as_dict() for payment in pr.payments]
|
||||||
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
pr.reconcile()
|
||||||
|
|
||||||
|
pe.reload()
|
||||||
|
self.assertEqual(len(pe.references), 1)
|
||||||
|
self.assertEqual(pe.references[0].exchange_rate, 1)
|
||||||
|
# There should not be any Exc Gain/Loss
|
||||||
|
self.assertEqual(pe.references[0].exchange_gain_loss, 0)
|
||||||
|
self.assertEqual(pe.references[0].reference_name, reverse_pe.name)
|
||||||
|
|
||||||
|
journals = frappe.db.get_all(
|
||||||
|
"Journal Entry",
|
||||||
|
filters={
|
||||||
|
"voucher_type": "Exchange Gain Or Loss",
|
||||||
|
"reference_type": "Payment Entry",
|
||||||
|
"reference_name": ("in", [pe.name, reverse_pe.name]),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# There should be no Exchange Gain/Loss created
|
||||||
|
self.assertEqual(journals, [])
|
||||||
|
|
||||||
|
def test_advance_reverse_payment_against_payment_for_supplier(self):
|
||||||
|
"""
|
||||||
|
Reconcile an Advance payment against reverse payment, for a supplier.
|
||||||
|
"""
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Company",
|
||||||
|
self.company,
|
||||||
|
{
|
||||||
|
"book_advance_payments_in_separate_party_account": 1,
|
||||||
|
"default_advance_paid_account": self.advance_payable_account,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.supplier = "_Test Supplier"
|
||||||
|
amount = 4000
|
||||||
|
|
||||||
|
pe = self.create_payment_entry(amount=amount)
|
||||||
|
pe.party_type = "Supplier"
|
||||||
|
pe.party = self.supplier
|
||||||
|
pe.payment_type = "Pay"
|
||||||
|
pe.paid_from = self.cash
|
||||||
|
pe.paid_to = self.advance_payable_account
|
||||||
|
pe.save().submit()
|
||||||
|
|
||||||
|
reverse_pe = self.create_payment_entry(amount=amount)
|
||||||
|
reverse_pe.party_type = "Supplier"
|
||||||
|
reverse_pe.party = self.supplier
|
||||||
|
reverse_pe.payment_type = "Receive"
|
||||||
|
reverse_pe.paid_from = self.advance_payable_account
|
||||||
|
reverse_pe.paid_to = self.cash
|
||||||
|
reverse_pe.save().submit()
|
||||||
|
|
||||||
|
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||||
|
pr.default_advance_account = self.advance_payable_account
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
self.assertEqual(len(pr.invoices), 1)
|
||||||
|
self.assertEqual(len(pr.payments), 1)
|
||||||
|
self.assertEqual(pr.invoices[0].invoice_number, reverse_pe.name)
|
||||||
|
self.assertEqual(pr.payments[0].reference_name, pe.name)
|
||||||
|
|
||||||
|
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||||
|
payments = [payment.as_dict() for payment in pr.payments]
|
||||||
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
pr.reconcile()
|
||||||
|
|
||||||
|
pe.reload()
|
||||||
|
self.assertEqual(len(pe.references), 1)
|
||||||
|
self.assertEqual(pe.references[0].exchange_rate, 1)
|
||||||
|
# There should not be any Exc Gain/Loss
|
||||||
|
self.assertEqual(pe.references[0].exchange_gain_loss, 0)
|
||||||
|
self.assertEqual(pe.references[0].reference_name, reverse_pe.name)
|
||||||
|
|
||||||
|
journals = frappe.db.get_all(
|
||||||
|
"Journal Entry",
|
||||||
|
filters={
|
||||||
|
"voucher_type": "Exchange Gain Or Loss",
|
||||||
|
"reference_type": "Payment Entry",
|
||||||
|
"reference_name": ("in", [pe.name, reverse_pe.name]),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# There should be no Exchange Gain/Loss created
|
||||||
|
self.assertEqual(journals, [])
|
||||||
|
|
||||||
|
# Assert Ledger Entries
|
||||||
|
gl_entries = frappe.db.get_all(
|
||||||
|
"GL Entry",
|
||||||
|
filters={"voucher_no": pe.name},
|
||||||
|
fields=["account", "voucher_no", "against_voucher", "debit", "credit"],
|
||||||
|
order_by="account, against_voucher, debit",
|
||||||
|
)
|
||||||
|
expected_gle = [
|
||||||
|
{
|
||||||
|
"account": self.advance_payable_account,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher": pe.name,
|
||||||
|
"debit": 0.0,
|
||||||
|
"credit": amount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": self.advance_payable_account,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher": pe.name,
|
||||||
|
"debit": amount,
|
||||||
|
"credit": 0.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": self.advance_payable_account,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher": reverse_pe.name,
|
||||||
|
"debit": amount,
|
||||||
|
"credit": 0.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": "Cash - _PR",
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher": None,
|
||||||
|
"debit": 0.0,
|
||||||
|
"credit": amount,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
self.assertEqual(gl_entries, expected_gle)
|
||||||
|
pl_entries = frappe.db.get_all(
|
||||||
|
"Payment Ledger Entry",
|
||||||
|
filters={"voucher_no": pe.name},
|
||||||
|
fields=["account", "voucher_no", "against_voucher_no", "amount"],
|
||||||
|
order_by="account, against_voucher_no, amount",
|
||||||
|
)
|
||||||
|
expected_ple = [
|
||||||
|
{
|
||||||
|
"account": self.advance_payable_account,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher_no": pe.name,
|
||||||
|
"amount": -amount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": self.advance_payable_account,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher_no": pe.name,
|
||||||
|
"amount": amount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": self.advance_payable_account,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher_no": reverse_pe.name,
|
||||||
|
"amount": -amount,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
self.assertEqual(pl_entries, expected_ple)
|
||||||
|
|
||||||
|
|
||||||
def make_customer(customer_name, currency=None):
|
def make_customer(customer_name, currency=None):
|
||||||
if not frappe.db.exists("Customer", customer_name):
|
if not frappe.db.exists("Customer", customer_name):
|
||||||
|
|||||||
@@ -149,35 +149,37 @@ class PaymentRequest(Document):
|
|||||||
).format(self.grand_total, amount)
|
).format(self.grand_total, amount)
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_submit(self):
|
def on_change(self):
|
||||||
if self.payment_request_type == "Outward":
|
|
||||||
self.db_set("status", "Initiated")
|
|
||||||
return
|
|
||||||
elif self.payment_request_type == "Inward":
|
|
||||||
self.db_set("status", "Requested")
|
|
||||||
|
|
||||||
send_mail = self.payment_gateway_validation() if self.payment_gateway else None
|
|
||||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||||
|
|
||||||
if (
|
|
||||||
hasattr(ref_doc, "order_type") and ref_doc.order_type == "Shopping Cart"
|
|
||||||
) or self.flags.mute_email:
|
|
||||||
send_mail = False
|
|
||||||
|
|
||||||
if send_mail and self.payment_channel != "Phone":
|
|
||||||
self.set_payment_request_url()
|
|
||||||
self.send_email()
|
|
||||||
self.make_communication_entry()
|
|
||||||
|
|
||||||
elif self.payment_channel == "Phone":
|
|
||||||
self.request_phone_payment()
|
|
||||||
|
|
||||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
|
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
|
||||||
"advance_payment_payable_doctypes"
|
"advance_payment_payable_doctypes"
|
||||||
)
|
)
|
||||||
if self.reference_doctype in advance_payment_doctypes:
|
if self.reference_doctype in advance_payment_doctypes:
|
||||||
# set advance payment status
|
# set advance payment status
|
||||||
ref_doc.set_total_advance_paid()
|
ref_doc.set_advance_payment_status()
|
||||||
|
|
||||||
|
def on_submit(self):
|
||||||
|
if self.payment_request_type == "Outward":
|
||||||
|
self.db_set("status", "Initiated")
|
||||||
|
elif self.payment_request_type == "Inward":
|
||||||
|
self.db_set("status", "Requested")
|
||||||
|
|
||||||
|
if self.payment_request_type == "Inward":
|
||||||
|
send_mail = self.payment_gateway_validation() if self.payment_gateway else None
|
||||||
|
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||||
|
|
||||||
|
if (
|
||||||
|
hasattr(ref_doc, "order_type") and ref_doc.order_type == "Shopping Cart"
|
||||||
|
) or self.flags.mute_email:
|
||||||
|
send_mail = False
|
||||||
|
|
||||||
|
if send_mail and self.payment_channel != "Phone":
|
||||||
|
self.set_payment_request_url()
|
||||||
|
self.send_email()
|
||||||
|
self.make_communication_entry()
|
||||||
|
|
||||||
|
elif self.payment_channel == "Phone":
|
||||||
|
self.request_phone_payment()
|
||||||
|
|
||||||
def request_phone_payment(self):
|
def request_phone_payment(self):
|
||||||
controller = _get_payment_gateway_controller(self.payment_gateway)
|
controller = _get_payment_gateway_controller(self.payment_gateway)
|
||||||
@@ -217,14 +219,6 @@ class PaymentRequest(Document):
|
|||||||
self.check_if_payment_entry_exists()
|
self.check_if_payment_entry_exists()
|
||||||
self.set_as_cancelled()
|
self.set_as_cancelled()
|
||||||
|
|
||||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
|
||||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
|
|
||||||
"advance_payment_payable_doctypes"
|
|
||||||
)
|
|
||||||
if self.reference_doctype in advance_payment_doctypes:
|
|
||||||
# set advance payment status
|
|
||||||
ref_doc.set_total_advance_paid()
|
|
||||||
|
|
||||||
def make_invoice(self):
|
def make_invoice(self):
|
||||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||||
if hasattr(ref_doc, "order_type") and ref_doc.order_type == "Shopping Cart":
|
if hasattr(ref_doc, "order_type") and ref_doc.order_type == "Shopping Cart":
|
||||||
@@ -641,7 +635,11 @@ def update_payment_req_status(doc, method):
|
|||||||
|
|
||||||
if payment_request_name:
|
if payment_request_name:
|
||||||
ref_details = get_reference_details(
|
ref_details = get_reference_details(
|
||||||
ref.reference_doctype, ref.reference_name, doc.party_account_currency
|
ref.reference_doctype,
|
||||||
|
ref.reference_name,
|
||||||
|
doc.party_account_currency,
|
||||||
|
doc.party_type,
|
||||||
|
doc.party,
|
||||||
)
|
)
|
||||||
pay_req_doc = frappe.get_doc("Payment Request", payment_request_name)
|
pay_req_doc = frappe.get_doc("Payment Request", payment_request_name)
|
||||||
status = pay_req_doc.status
|
status = pay_req_doc.status
|
||||||
|
|||||||
@@ -35,10 +35,11 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:11.676098",
|
"modified": "2024-04-07 11:26:42.021585",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Terms Template",
|
"name": "Payment Terms Template",
|
||||||
|
"naming_rule": "By fieldname",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
@@ -76,6 +77,15 @@
|
|||||||
"role": "Accounts Manager",
|
"role": "Accounts Manager",
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "All",
|
||||||
|
"select": 1,
|
||||||
|
"share": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
|
|||||||
@@ -441,6 +441,7 @@ class POSInvoice(SalesInvoice):
|
|||||||
|
|
||||||
if self.is_return:
|
if self.is_return:
|
||||||
invoice_total = self.rounded_total or self.grand_total
|
invoice_total = self.rounded_total or self.grand_total
|
||||||
|
total_amount_in_payments = flt(total_amount_in_payments, self.precision("grand_total"))
|
||||||
if total_amount_in_payments and total_amount_in_payments < invoice_total:
|
if total_amount_in_payments and total_amount_in_payments < invoice_total:
|
||||||
frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total))
|
frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total))
|
||||||
|
|
||||||
|
|||||||
@@ -136,7 +136,11 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
|||||||
|
|
||||||
if (!doc.is_return && doc.docstatus == 1) {
|
if (!doc.is_return && doc.docstatus == 1) {
|
||||||
if (doc.outstanding_amount >= 0 || Math.abs(flt(doc.outstanding_amount)) < flt(doc.grand_total)) {
|
if (doc.outstanding_amount >= 0 || Math.abs(flt(doc.outstanding_amount)) < flt(doc.grand_total)) {
|
||||||
this.frm.add_custom_button(__("Return / Debit Note"), this.make_debit_note, __("Create"));
|
this.frm.add_custom_button(
|
||||||
|
__("Return / Debit Note"),
|
||||||
|
this.make_debit_note.bind(this),
|
||||||
|
__("Create")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"col_break1",
|
"col_break1",
|
||||||
"account_head",
|
"account_head",
|
||||||
"description",
|
"description",
|
||||||
|
"is_tax_withholding_account",
|
||||||
"section_break_10",
|
"section_break_10",
|
||||||
"rate",
|
"rate",
|
||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
@@ -225,15 +226,23 @@
|
|||||||
"label": "Account Currency",
|
"label": "Account Currency",
|
||||||
"options": "Currency",
|
"options": "Currency",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_tax_withholding_account",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Tax Withholding Account",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:26.775139",
|
"modified": "2024-04-08 19:51:36.678551",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Taxes and Charges",
|
"name": "Purchase Taxes and Charges",
|
||||||
|
"naming_rule": "Random",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class PurchaseTaxesandCharges(Document):
|
|||||||
description: DF.SmallText
|
description: DF.SmallText
|
||||||
included_in_paid_amount: DF.Check
|
included_in_paid_amount: DF.Check
|
||||||
included_in_print_rate: DF.Check
|
included_in_print_rate: DF.Check
|
||||||
|
is_tax_withholding_account: DF.Check
|
||||||
item_wise_tax_detail: DF.Code | None
|
item_wise_tax_detail: DF.Code | None
|
||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
|
|||||||
@@ -118,7 +118,11 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (doc.outstanding_amount >= 0 || Math.abs(flt(doc.outstanding_amount)) < flt(doc.grand_total)) {
|
if (doc.outstanding_amount >= 0 || Math.abs(flt(doc.outstanding_amount)) < flt(doc.grand_total)) {
|
||||||
this.frm.add_custom_button(__("Return / Credit Note"), this.make_sales_return, __("Create"));
|
this.frm.add_custom_button(
|
||||||
|
__("Return / Credit Note"),
|
||||||
|
this.make_sales_return.bind(this),
|
||||||
|
__("Create")
|
||||||
|
);
|
||||||
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
|
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3632,6 +3632,105 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
self.assertEqual(1, len(advances))
|
self.assertEqual(1, len(advances))
|
||||||
self.assertEqual(advances[0].reference_name, pe.name)
|
self.assertEqual(advances[0].reference_name, pe.name)
|
||||||
|
|
||||||
|
def test_taxes_merging_from_delivery_note(self):
|
||||||
|
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||||
|
|
||||||
|
dn1 = create_delivery_note(do_not_submit=1)
|
||||||
|
dn1.items[0].qty = 10
|
||||||
|
dn1.items[0].rate = 100
|
||||||
|
dn1.append(
|
||||||
|
"taxes",
|
||||||
|
{
|
||||||
|
"charge_type": "Actual",
|
||||||
|
"account_head": "Freight and Forwarding Charges - _TC",
|
||||||
|
"description": "movement charges",
|
||||||
|
"tax_amount": 100,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
dn1.append(
|
||||||
|
"taxes",
|
||||||
|
{
|
||||||
|
"charge_type": "Actual",
|
||||||
|
"account_head": "Marketing Expenses - _TC",
|
||||||
|
"description": "marketing",
|
||||||
|
"tax_amount": 150,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
dn1.save().submit()
|
||||||
|
|
||||||
|
dn2 = create_delivery_note(do_not_submit=1)
|
||||||
|
dn2.items[0].qty = 5
|
||||||
|
dn2.items[0].rate = 100
|
||||||
|
dn2.append(
|
||||||
|
"taxes",
|
||||||
|
{
|
||||||
|
"charge_type": "Actual",
|
||||||
|
"account_head": "Freight and Forwarding Charges - _TC",
|
||||||
|
"description": "movement charges",
|
||||||
|
"tax_amount": 20,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
dn2.append(
|
||||||
|
"taxes",
|
||||||
|
{
|
||||||
|
"charge_type": "Actual",
|
||||||
|
"account_head": "Miscellaneous Expenses - _TC",
|
||||||
|
"description": "marketing",
|
||||||
|
"tax_amount": 60,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
dn2.save().submit()
|
||||||
|
|
||||||
|
# si = make_sales_invoice(dn1.name)
|
||||||
|
si = create_sales_invoice(do_not_submit=True)
|
||||||
|
si.customer = dn1.customer
|
||||||
|
si.items.clear()
|
||||||
|
|
||||||
|
from frappe.model.mapper import map_docs
|
||||||
|
|
||||||
|
map_docs(
|
||||||
|
method="erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
|
||||||
|
source_names=frappe.json.dumps([dn1.name, dn2.name]),
|
||||||
|
target_doc=si,
|
||||||
|
args=frappe.json.dumps({"customer": dn1.customer, "merge_taxes": 1, "filtered_children": []}),
|
||||||
|
)
|
||||||
|
si.save().submit()
|
||||||
|
|
||||||
|
expected = [
|
||||||
|
{
|
||||||
|
"charge_type": "Actual",
|
||||||
|
"account_head": "Freight and Forwarding Charges - _TC",
|
||||||
|
"tax_amount": 120.0,
|
||||||
|
"total": 1520.0,
|
||||||
|
"base_total": 1520.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"charge_type": "Actual",
|
||||||
|
"account_head": "Marketing Expenses - _TC",
|
||||||
|
"tax_amount": 150.0,
|
||||||
|
"total": 1670.0,
|
||||||
|
"base_total": 1670.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"charge_type": "Actual",
|
||||||
|
"account_head": "Miscellaneous Expenses - _TC",
|
||||||
|
"tax_amount": 60.0,
|
||||||
|
"total": 1610.0,
|
||||||
|
"base_total": 1610.0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
actual = [
|
||||||
|
dict(
|
||||||
|
charge_type=x.charge_type,
|
||||||
|
account_head=x.account_head,
|
||||||
|
tax_amount=x.tax_amount,
|
||||||
|
total=x.total,
|
||||||
|
base_total=x.base_total,
|
||||||
|
)
|
||||||
|
for x in si.taxes
|
||||||
|
]
|
||||||
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
|
|
||||||
def set_advance_flag(company, flag, default_account):
|
def set_advance_flag(company, flag, default_account):
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
|
|||||||
@@ -146,7 +146,12 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
|||||||
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
|
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
|
||||||
|
|
||||||
cost_center = get_cost_center(inv)
|
cost_center = get_cost_center(inv)
|
||||||
tax_row.update({"cost_center": cost_center})
|
tax_row.update(
|
||||||
|
{
|
||||||
|
"cost_center": cost_center,
|
||||||
|
"is_tax_withholding_account": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if inv.doctype == "Purchase Invoice":
|
if inv.doctype == "Purchase Invoice":
|
||||||
return tax_row, tax_deducted_on_advances, voucher_wise_amount
|
return tax_row, tax_deducted_on_advances, voucher_wise_amount
|
||||||
|
|||||||
@@ -55,10 +55,10 @@
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: right">
|
<td style="text-align: right">
|
||||||
{%= format_currency(data[i].debit, filters.presentation_currency) %}
|
{%= format_currency(data[i].debit, filters.presentation_currency || data[i].account_currency) %}
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: right">
|
<td style="text-align: right">
|
||||||
{%= format_currency(data[i].credit, filters.presentation_currency) %}
|
{%= format_currency(data[i].credit, filters.presentation_currency || data[i].account_currency) %}
|
||||||
</td>
|
</td>
|
||||||
{% } else { %}
|
{% } else { %}
|
||||||
<td></td>
|
<td></td>
|
||||||
|
|||||||
@@ -347,7 +347,7 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
|
|||||||
# acc
|
# acc
|
||||||
if acc_dict.entries:
|
if acc_dict.entries:
|
||||||
# opening
|
# opening
|
||||||
data.append({})
|
data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None})
|
||||||
if filters.get("group_by") != "Group by Voucher":
|
if filters.get("group_by") != "Group by Voucher":
|
||||||
data.append(acc_dict.totals.opening)
|
data.append(acc_dict.totals.opening)
|
||||||
|
|
||||||
@@ -359,7 +359,8 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
|
|||||||
# closing
|
# closing
|
||||||
if filters.get("group_by") != "Group by Voucher":
|
if filters.get("group_by") != "Group by Voucher":
|
||||||
data.append(acc_dict.totals.closing)
|
data.append(acc_dict.totals.closing)
|
||||||
data.append({})
|
|
||||||
|
data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None})
|
||||||
else:
|
else:
|
||||||
data += entries
|
data += entries
|
||||||
|
|
||||||
@@ -380,6 +381,8 @@ def get_totals_dict():
|
|||||||
credit=0.0,
|
credit=0.0,
|
||||||
debit_in_account_currency=0.0,
|
debit_in_account_currency=0.0,
|
||||||
credit_in_account_currency=0.0,
|
credit_in_account_currency=0.0,
|
||||||
|
debit_in_transaction_currency=None,
|
||||||
|
credit_in_transaction_currency=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
return _dict(
|
return _dict(
|
||||||
@@ -424,6 +427,10 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
|||||||
data[key].debit_in_account_currency += gle.debit_in_account_currency
|
data[key].debit_in_account_currency += gle.debit_in_account_currency
|
||||||
data[key].credit_in_account_currency += gle.credit_in_account_currency
|
data[key].credit_in_account_currency += gle.credit_in_account_currency
|
||||||
|
|
||||||
|
if filters.get("add_values_in_transaction_currency") and key not in ["opening", "closing", "total"]:
|
||||||
|
data[key].debit_in_transaction_currency += gle.debit_in_transaction_currency
|
||||||
|
data[key].credit_in_transaction_currency += gle.credit_in_transaction_currency
|
||||||
|
|
||||||
if filters.get("show_net_values_in_party_account") and account_type_map.get(data[key].account) in (
|
if filters.get("show_net_values_in_party_account") and account_type_map.get(data[key].account) in (
|
||||||
"Receivable",
|
"Receivable",
|
||||||
"Payable",
|
"Payable",
|
||||||
|
|||||||
@@ -61,10 +61,11 @@ def get_pos_entries(filters, group_by_field):
|
|||||||
order_by += f", p.{group_by_field}"
|
order_by += f", p.{group_by_field}"
|
||||||
select_mop_field = ", p.base_paid_amount - p.change_amount as paid_amount "
|
select_mop_field = ", p.base_paid_amount - p.change_amount as paid_amount "
|
||||||
|
|
||||||
|
# nosemgrep
|
||||||
return frappe.db.sql(
|
return frappe.db.sql(
|
||||||
f"""
|
f"""
|
||||||
SELECT
|
SELECT
|
||||||
p.posting_date, p.name as pos_invoice, p.pos_profile,
|
p.posting_date, p.name as pos_invoice, p.pos_profile, p.company,
|
||||||
p.owner, p.customer, p.is_return, p.base_grand_total as grand_total {select_mop_field}
|
p.owner, p.customer, p.is_return, p.base_grand_total as grand_total {select_mop_field}
|
||||||
FROM
|
FROM
|
||||||
`tabPOS Invoice` p {from_sales_invoice_payment}
|
`tabPOS Invoice` p {from_sales_invoice_payment}
|
||||||
@@ -201,14 +202,14 @@ def get_columns(filters):
|
|||||||
"label": _("Grand Total"),
|
"label": _("Grand Total"),
|
||||||
"fieldname": "grand_total",
|
"fieldname": "grand_total",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"options": "company:currency",
|
"options": "Company:company:default_currency",
|
||||||
"width": 120,
|
"width": 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": _("Paid Amount"),
|
"label": _("Paid Amount"),
|
||||||
"fieldname": "paid_amount",
|
"fieldname": "paid_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"options": "company:currency",
|
"options": "Company:company:default_currency",
|
||||||
"width": 120,
|
"width": 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -218,6 +219,13 @@ def get_columns(filters):
|
|||||||
"width": 150,
|
"width": 150,
|
||||||
},
|
},
|
||||||
{"label": _("Is Return"), "fieldname": "is_return", "fieldtype": "Data", "width": 80},
|
{"label": _("Is Return"), "fieldname": "is_return", "fieldtype": "Data", "width": 80},
|
||||||
|
{
|
||||||
|
"label": _("Company"),
|
||||||
|
"fieldname": "company",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Company",
|
||||||
|
"width": 120,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return columns
|
return columns
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ from frappe.query_builder import AliasedQuery, Criterion, Table
|
|||||||
from frappe.query_builder.functions import Round, Sum
|
from frappe.query_builder.functions import Round, Sum
|
||||||
from frappe.query_builder.utils import DocType
|
from frappe.query_builder.utils import DocType
|
||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
|
add_days,
|
||||||
cint,
|
cint,
|
||||||
create_batch,
|
create_batch,
|
||||||
cstr,
|
cstr,
|
||||||
flt,
|
flt,
|
||||||
formatdate,
|
formatdate,
|
||||||
|
get_datetime,
|
||||||
get_number_format_info,
|
get_number_format_info,
|
||||||
getdate,
|
getdate,
|
||||||
now,
|
now,
|
||||||
@@ -720,7 +722,19 @@ def update_reference_in_payment_entry(
|
|||||||
payment_entry.setup_party_account_field()
|
payment_entry.setup_party_account_field()
|
||||||
payment_entry.set_missing_values()
|
payment_entry.set_missing_values()
|
||||||
if not skip_ref_details_update_for_pe:
|
if not skip_ref_details_update_for_pe:
|
||||||
payment_entry.set_missing_ref_details(ref_exchange_rate=d.exchange_rate or None)
|
reference_exchange_details = frappe._dict()
|
||||||
|
if d.against_voucher_type == "Journal Entry" and d.exchange_rate:
|
||||||
|
reference_exchange_details.update(
|
||||||
|
{
|
||||||
|
"reference_doctype": d.against_voucher_type,
|
||||||
|
"reference_name": d.against_voucher,
|
||||||
|
"exchange_rate": d.exchange_rate,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
payment_entry.set_missing_ref_details(
|
||||||
|
update_ref_details_only_for=[(d.against_voucher_type, d.against_voucher)],
|
||||||
|
reference_exchange_details=reference_exchange_details,
|
||||||
|
)
|
||||||
payment_entry.set_amounts()
|
payment_entry.set_amounts()
|
||||||
|
|
||||||
payment_entry.make_exchange_gain_loss_journal(
|
payment_entry.make_exchange_gain_loss_journal(
|
||||||
@@ -1709,6 +1723,7 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa
|
|||||||
)
|
)
|
||||||
|
|
||||||
ref_doc.set_status(update=True)
|
ref_doc.set_status(update=True)
|
||||||
|
ref_doc.notify_update()
|
||||||
|
|
||||||
|
|
||||||
def delink_original_entry(pl_entry, partial_cancel=False):
|
def delink_original_entry(pl_entry, partial_cancel=False):
|
||||||
@@ -2059,3 +2074,44 @@ def create_gain_loss_journal(
|
|||||||
|
|
||||||
def get_party_types_from_account_type(account_type):
|
def get_party_types_from_account_type(account_type):
|
||||||
return frappe.db.get_all("Party Type", {"account_type": account_type}, pluck="name")
|
return frappe.db.get_all("Party Type", {"account_type": account_type}, pluck="name")
|
||||||
|
|
||||||
|
|
||||||
|
def run_ledger_health_checks():
|
||||||
|
health_monitor_settings = frappe.get_doc("Ledger Health Monitor")
|
||||||
|
if health_monitor_settings.enable_health_monitor:
|
||||||
|
period_end = getdate()
|
||||||
|
period_start = add_days(period_end, -abs(health_monitor_settings.monitor_for_last_x_days))
|
||||||
|
|
||||||
|
run_date = get_datetime()
|
||||||
|
|
||||||
|
# Debit-Credit mismatch report
|
||||||
|
if health_monitor_settings.debit_credit_mismatch:
|
||||||
|
for x in health_monitor_settings.companies:
|
||||||
|
filters = {"company": x.company, "from_date": period_start, "to_date": period_end}
|
||||||
|
voucher_wise = frappe.get_doc("Report", "Voucher-wise Balance")
|
||||||
|
res = voucher_wise.execute_script_report(filters=filters)
|
||||||
|
for x in res[1]:
|
||||||
|
doc = frappe.new_doc("Ledger Health")
|
||||||
|
doc.voucher_type = x.voucher_type
|
||||||
|
doc.voucher_no = x.voucher_no
|
||||||
|
doc.debit_credit_mismatch = True
|
||||||
|
doc.checked_on = run_date
|
||||||
|
doc.save()
|
||||||
|
|
||||||
|
# General Ledger and Payment Ledger discrepancy
|
||||||
|
if health_monitor_settings.general_and_payment_ledger_mismatch:
|
||||||
|
for x in health_monitor_settings.companies:
|
||||||
|
filters = {
|
||||||
|
"company": x.company,
|
||||||
|
"period_start_date": period_start,
|
||||||
|
"period_end_date": period_end,
|
||||||
|
}
|
||||||
|
gl_pl_comparison = frappe.get_doc("Report", "General and Payment Ledger Comparison")
|
||||||
|
res = gl_pl_comparison.execute_script_report(filters=filters)
|
||||||
|
for x in res[1]:
|
||||||
|
doc = frappe.new_doc("Ledger Health")
|
||||||
|
doc.voucher_type = x.voucher_type
|
||||||
|
doc.voucher_no = x.voucher_no
|
||||||
|
doc.general_and_payment_ledger_mismatch = True
|
||||||
|
doc.checked_on = run_date
|
||||||
|
doc.save()
|
||||||
|
|||||||
@@ -232,7 +232,11 @@ class TestAsset(AssetSetup):
|
|||||||
asset.precision("gross_purchase_amount"),
|
asset.precision("gross_purchase_amount"),
|
||||||
)
|
)
|
||||||
pro_rata_amount, _, _ = _get_pro_rata_amt(
|
pro_rata_amount, _, _ = _get_pro_rata_amt(
|
||||||
asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date
|
asset.finance_books[0],
|
||||||
|
9000,
|
||||||
|
get_last_day(add_months(purchase_date, 1)),
|
||||||
|
date,
|
||||||
|
original_schedule_date=get_last_day(nowdate()),
|
||||||
)
|
)
|
||||||
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
|
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -314,7 +318,11 @@ class TestAsset(AssetSetup):
|
|||||||
self.assertEqual(first_asset_depr_schedule.status, "Cancelled")
|
self.assertEqual(first_asset_depr_schedule.status, "Cancelled")
|
||||||
|
|
||||||
pro_rata_amount, _, _ = _get_pro_rata_amt(
|
pro_rata_amount, _, _ = _get_pro_rata_amt(
|
||||||
asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date
|
asset.finance_books[0],
|
||||||
|
9000,
|
||||||
|
get_last_day(add_months(purchase_date, 1)),
|
||||||
|
date,
|
||||||
|
original_schedule_date=get_last_day(nowdate()),
|
||||||
)
|
)
|
||||||
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
|
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
|
||||||
|
|
||||||
@@ -332,7 +340,6 @@ class TestAsset(AssetSetup):
|
|||||||
),
|
),
|
||||||
("Debtors - _TC", 25000.0, 0.0),
|
("Debtors - _TC", 25000.0, 0.0),
|
||||||
)
|
)
|
||||||
|
|
||||||
gle = get_gl_entries("Sales Invoice", si.name)
|
gle = get_gl_entries("Sales Invoice", si.name)
|
||||||
self.assertSequenceEqual(gle, expected_gle)
|
self.assertSequenceEqual(gle, expected_gle)
|
||||||
|
|
||||||
@@ -378,7 +385,7 @@ class TestAsset(AssetSetup):
|
|||||||
|
|
||||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
|
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
|
||||||
|
|
||||||
expected_values = [["2023-03-31", 12000, 36000], ["2023-05-23", 1742.47, 37742.47]]
|
expected_values = [["2023-03-31", 12000, 36000], ["2023-05-23", 1737.7, 37737.7]]
|
||||||
|
|
||||||
second_asset_depr_schedule = get_depr_schedule(asset.name, "Active")
|
second_asset_depr_schedule = get_depr_schedule(asset.name, "Active")
|
||||||
|
|
||||||
@@ -391,7 +398,7 @@ class TestAsset(AssetSetup):
|
|||||||
expected_gle = (
|
expected_gle = (
|
||||||
(
|
(
|
||||||
"_Test Accumulated Depreciations - _TC",
|
"_Test Accumulated Depreciations - _TC",
|
||||||
37742.47,
|
37737.7,
|
||||||
0.0,
|
0.0,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -402,7 +409,7 @@ class TestAsset(AssetSetup):
|
|||||||
(
|
(
|
||||||
"_Test Gain/Loss on Asset Disposal - _TC",
|
"_Test Gain/Loss on Asset Disposal - _TC",
|
||||||
0.0,
|
0.0,
|
||||||
17742.47,
|
17737.7,
|
||||||
),
|
),
|
||||||
("Debtors - _TC", 40000.0, 0.0),
|
("Debtors - _TC", 40000.0, 0.0),
|
||||||
)
|
)
|
||||||
@@ -707,25 +714,24 @@ class TestDepreciationMethods(AssetSetup):
|
|||||||
)
|
)
|
||||||
|
|
||||||
expected_schedules = [
|
expected_schedules = [
|
||||||
["2023-01-31", 1021.98, 1021.98],
|
["2023-01-31", 1019.18, 1019.18],
|
||||||
["2023-02-28", 923.08, 1945.06],
|
["2023-02-28", 920.55, 1939.73],
|
||||||
["2023-03-31", 1021.98, 2967.04],
|
["2023-03-31", 1019.18, 2958.91],
|
||||||
["2023-04-30", 989.01, 3956.05],
|
["2023-04-30", 986.3, 3945.21],
|
||||||
["2023-05-31", 1021.98, 4978.03],
|
["2023-05-31", 1019.18, 4964.39],
|
||||||
["2023-06-30", 989.01, 5967.04],
|
["2023-06-30", 986.3, 5950.69],
|
||||||
["2023-07-31", 1021.98, 6989.02],
|
["2023-07-31", 1019.18, 6969.87],
|
||||||
["2023-08-31", 1021.98, 8011.0],
|
["2023-08-31", 1019.18, 7989.05],
|
||||||
["2023-09-30", 989.01, 9000.01],
|
["2023-09-30", 986.3, 8975.35],
|
||||||
["2023-10-31", 1021.98, 10021.99],
|
["2023-10-31", 1019.18, 9994.53],
|
||||||
["2023-11-30", 989.01, 11011.0],
|
["2023-11-30", 986.3, 10980.83],
|
||||||
["2023-12-31", 989.0, 12000.0],
|
["2023-12-31", 1019.17, 12000.0],
|
||||||
]
|
]
|
||||||
|
|
||||||
schedules = [
|
schedules = [
|
||||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
||||||
for d in get_depr_schedule(asset.name, "Draft")
|
for d in get_depr_schedule(asset.name, "Draft")
|
||||||
]
|
]
|
||||||
|
|
||||||
self.assertEqual(schedules, expected_schedules)
|
self.assertEqual(schedules, expected_schedules)
|
||||||
|
|
||||||
def test_schedule_for_straight_line_method_for_existing_asset(self):
|
def test_schedule_for_straight_line_method_for_existing_asset(self):
|
||||||
|
|||||||
@@ -313,7 +313,6 @@ class AssetDepreciationSchedule(Document):
|
|||||||
has_wdv_or_dd_non_yearly_pro_rata,
|
has_wdv_or_dd_non_yearly_pro_rata,
|
||||||
number_of_pending_depreciations,
|
number_of_pending_depreciations,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not has_pro_rata or (
|
if not has_pro_rata or (
|
||||||
n < (cint(final_number_of_depreciations) - 1) or final_number_of_depreciations == 2
|
n < (cint(final_number_of_depreciations) - 1) or final_number_of_depreciations == 2
|
||||||
):
|
):
|
||||||
@@ -338,6 +337,7 @@ class AssetDepreciationSchedule(Document):
|
|||||||
depreciation_amount,
|
depreciation_amount,
|
||||||
from_date,
|
from_date,
|
||||||
date_of_disposal,
|
date_of_disposal,
|
||||||
|
original_schedule_date=schedule_date,
|
||||||
)
|
)
|
||||||
|
|
||||||
if depreciation_amount > 0:
|
if depreciation_amount > 0:
|
||||||
@@ -565,23 +565,27 @@ def _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly=Fa
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_pro_rata_amt(row, depreciation_amount, from_date, to_date, has_wdv_or_dd_non_yearly_pro_rata=False):
|
def _get_pro_rata_amt(
|
||||||
|
row,
|
||||||
|
depreciation_amount,
|
||||||
|
from_date,
|
||||||
|
to_date,
|
||||||
|
has_wdv_or_dd_non_yearly_pro_rata=False,
|
||||||
|
original_schedule_date=None,
|
||||||
|
):
|
||||||
days = date_diff(to_date, from_date)
|
days = date_diff(to_date, from_date)
|
||||||
months = month_diff(to_date, from_date)
|
months = month_diff(to_date, from_date)
|
||||||
if has_wdv_or_dd_non_yearly_pro_rata:
|
if has_wdv_or_dd_non_yearly_pro_rata:
|
||||||
total_days = get_total_days(to_date, 12)
|
total_days = get_total_days(original_schedule_date or to_date, 12)
|
||||||
else:
|
else:
|
||||||
total_days = get_total_days(to_date, row.frequency_of_depreciation)
|
total_days = get_total_days(original_schedule_date or to_date, row.frequency_of_depreciation)
|
||||||
|
|
||||||
return (depreciation_amount * flt(days)) / flt(total_days), days, months
|
return (depreciation_amount * flt(days)) / flt(total_days), days, months
|
||||||
|
|
||||||
|
|
||||||
def get_total_days(date, frequency):
|
def get_total_days(date, frequency):
|
||||||
period_start_date = add_months(date, cint(frequency) * -1)
|
period_start_date = add_months(date, cint(frequency) * -1)
|
||||||
|
|
||||||
if is_last_day_of_the_month(date):
|
if is_last_day_of_the_month(date):
|
||||||
period_start_date = get_last_day(period_start_date)
|
period_start_date = get_last_day(period_start_date)
|
||||||
|
|
||||||
return date_diff(date, period_start_date)
|
return date_diff(date, period_start_date)
|
||||||
|
|
||||||
|
|
||||||
@@ -632,33 +636,37 @@ def get_straight_line_or_manual_depr_amount(
|
|||||||
# if the Depreciation Schedule is being modified after Asset Value Adjustment due to decrease in asset value
|
# if the Depreciation Schedule is being modified after Asset Value Adjustment due to decrease in asset value
|
||||||
elif asset.flags.decrease_in_asset_value_due_to_value_adjustment:
|
elif asset.flags.decrease_in_asset_value_due_to_value_adjustment:
|
||||||
if row.daily_prorata_based:
|
if row.daily_prorata_based:
|
||||||
daily_depr_amount = (
|
amount = flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
||||||
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
total_days = (
|
||||||
) / date_diff(
|
date_diff(
|
||||||
get_last_day(
|
|
||||||
add_months(
|
|
||||||
row.depreciation_start_date,
|
|
||||||
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
|
|
||||||
* row.frequency_of_depreciation,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
add_days(
|
|
||||||
get_last_day(
|
get_last_day(
|
||||||
add_months(
|
add_months(
|
||||||
row.depreciation_start_date,
|
row.depreciation_start_date,
|
||||||
flt(
|
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
|
||||||
row.total_number_of_depreciations
|
|
||||||
- asset.number_of_depreciations_booked
|
|
||||||
- number_of_pending_depreciations
|
|
||||||
- 1
|
|
||||||
)
|
|
||||||
* row.frequency_of_depreciation,
|
* row.frequency_of_depreciation,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
1,
|
add_days(
|
||||||
),
|
get_last_day(
|
||||||
|
add_months(
|
||||||
|
row.depreciation_start_date,
|
||||||
|
flt(
|
||||||
|
row.total_number_of_depreciations
|
||||||
|
- asset.number_of_depreciations_booked
|
||||||
|
- number_of_pending_depreciations
|
||||||
|
- 1
|
||||||
|
)
|
||||||
|
* row.frequency_of_depreciation,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
+ 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
daily_depr_amount = amount / total_days
|
||||||
|
|
||||||
to_date = get_last_day(
|
to_date = get_last_day(
|
||||||
add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
|
add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
|
||||||
)
|
)
|
||||||
@@ -679,24 +687,33 @@ def get_straight_line_or_manual_depr_amount(
|
|||||||
# if the Depreciation Schedule is being prepared for the first time
|
# if the Depreciation Schedule is being prepared for the first time
|
||||||
else:
|
else:
|
||||||
if row.daily_prorata_based:
|
if row.daily_prorata_based:
|
||||||
daily_depr_amount = (
|
amount = (
|
||||||
flt(asset.gross_purchase_amount)
|
flt(asset.gross_purchase_amount)
|
||||||
- flt(asset.opening_accumulated_depreciation)
|
- flt(asset.opening_accumulated_depreciation)
|
||||||
- flt(row.expected_value_after_useful_life)
|
- flt(row.expected_value_after_useful_life)
|
||||||
) / date_diff(
|
|
||||||
get_last_day(
|
|
||||||
add_months(
|
|
||||||
row.depreciation_start_date,
|
|
||||||
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
|
|
||||||
* row.frequency_of_depreciation,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
add_days(
|
|
||||||
get_last_day(add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation)),
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
total_days = (
|
||||||
|
date_diff(
|
||||||
|
get_last_day(
|
||||||
|
add_months(
|
||||||
|
row.depreciation_start_date,
|
||||||
|
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
|
||||||
|
* row.frequency_of_depreciation,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
add_days(
|
||||||
|
get_last_day(
|
||||||
|
add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation)
|
||||||
|
),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
+ 1
|
||||||
|
)
|
||||||
|
|
||||||
|
daily_depr_amount = amount / total_days
|
||||||
|
|
||||||
to_date = get_last_day(
|
to_date = get_last_day(
|
||||||
add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
|
add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
|
||||||
)
|
)
|
||||||
@@ -708,7 +725,6 @@ def get_straight_line_or_manual_depr_amount(
|
|||||||
),
|
),
|
||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
|
|
||||||
return daily_depr_amount * (date_diff(to_date, from_date) + 1)
|
return daily_depr_amount * (date_diff(to_date, from_date) + 1)
|
||||||
else:
|
else:
|
||||||
return (
|
return (
|
||||||
@@ -980,32 +996,35 @@ def get_depr_schedule(asset_name, status, finance_book=None):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_asset_depr_schedule_doc(asset_name, status, finance_book=None):
|
def get_asset_depr_schedule_doc(asset_name, status, finance_book=None):
|
||||||
asset_depr_schedule_name = get_asset_depr_schedule_name(asset_name, status, finance_book)
|
asset_depr_schedule = get_asset_depr_schedule_name(asset_name, status, finance_book)
|
||||||
|
|
||||||
if not asset_depr_schedule_name:
|
if not asset_depr_schedule:
|
||||||
return
|
return
|
||||||
|
|
||||||
asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule_name)
|
asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule[0].name)
|
||||||
|
|
||||||
return asset_depr_schedule_doc
|
return asset_depr_schedule_doc
|
||||||
|
|
||||||
|
|
||||||
def get_asset_depr_schedule_name(asset_name, status, finance_book=None):
|
def get_asset_depr_schedule_name(asset_name, status, finance_book=None):
|
||||||
if finance_book is None:
|
|
||||||
finance_book_filter = ["finance_book", "is", "not set"]
|
|
||||||
else:
|
|
||||||
finance_book_filter = ["finance_book", "=", finance_book]
|
|
||||||
|
|
||||||
if isinstance(status, str):
|
if isinstance(status, str):
|
||||||
status = [status]
|
status = [status]
|
||||||
|
|
||||||
return frappe.db.get_value(
|
filters = [
|
||||||
|
["asset", "=", asset_name],
|
||||||
|
["status", "in", status],
|
||||||
|
["docstatus", "<", 2],
|
||||||
|
]
|
||||||
|
|
||||||
|
if finance_book:
|
||||||
|
filters.append(["finance_book", "=", finance_book])
|
||||||
|
else:
|
||||||
|
filters.append(["finance_book", "is", "not set"])
|
||||||
|
|
||||||
|
return frappe.get_all(
|
||||||
doctype="Asset Depreciation Schedule",
|
doctype="Asset Depreciation Schedule",
|
||||||
filters=[
|
filters=filters,
|
||||||
["asset", "=", asset_name],
|
limit=1,
|
||||||
finance_book_filter,
|
|
||||||
["status", "in", status],
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ def get_data(filters):
|
|||||||
"asset_category": asset.asset_category,
|
"asset_category": asset.asset_category,
|
||||||
"purchase_date": asset.purchase_date,
|
"purchase_date": asset.purchase_date,
|
||||||
"asset_value": asset_value,
|
"asset_value": asset_value,
|
||||||
|
"company": asset.company,
|
||||||
}
|
}
|
||||||
data.append(row)
|
data.append(row)
|
||||||
|
|
||||||
@@ -369,30 +370,37 @@ def get_columns(filters):
|
|||||||
"label": _("Gross Purchase Amount"),
|
"label": _("Gross Purchase Amount"),
|
||||||
"fieldname": "gross_purchase_amount",
|
"fieldname": "gross_purchase_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"options": "company:currency",
|
"options": "Company:company:default_currency",
|
||||||
"width": 250,
|
"width": 250,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": _("Opening Accumulated Depreciation"),
|
"label": _("Opening Accumulated Depreciation"),
|
||||||
"fieldname": "opening_accumulated_depreciation",
|
"fieldname": "opening_accumulated_depreciation",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"options": "company:currency",
|
"options": "Company:company:default_currency",
|
||||||
"width": 250,
|
"width": 250,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": _("Depreciated Amount"),
|
"label": _("Depreciated Amount"),
|
||||||
"fieldname": "depreciated_amount",
|
"fieldname": "depreciated_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"options": "company:currency",
|
"options": "Company:company:default_currency",
|
||||||
"width": 250,
|
"width": 250,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": _("Asset Value"),
|
"label": _("Asset Value"),
|
||||||
"fieldname": "asset_value",
|
"fieldname": "asset_value",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"options": "company:currency",
|
"options": "Company:company:default_currency",
|
||||||
"width": 250,
|
"width": 250,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": _("Company"),
|
||||||
|
"fieldname": "company",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Company",
|
||||||
|
"width": 120,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -423,28 +431,28 @@ def get_columns(filters):
|
|||||||
"label": _("Gross Purchase Amount"),
|
"label": _("Gross Purchase Amount"),
|
||||||
"fieldname": "gross_purchase_amount",
|
"fieldname": "gross_purchase_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"options": "company:currency",
|
"options": "Company:company:default_currency",
|
||||||
"width": 100,
|
"width": 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": _("Asset Value"),
|
"label": _("Asset Value"),
|
||||||
"fieldname": "asset_value",
|
"fieldname": "asset_value",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"options": "company:currency",
|
"options": "Company:company:default_currency",
|
||||||
"width": 100,
|
"width": 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": _("Opening Accumulated Depreciation"),
|
"label": _("Opening Accumulated Depreciation"),
|
||||||
"fieldname": "opening_accumulated_depreciation",
|
"fieldname": "opening_accumulated_depreciation",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"options": "company:currency",
|
"options": "Company:company:default_currency",
|
||||||
"width": 90,
|
"width": 90,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": _("Depreciated Amount"),
|
"label": _("Depreciated Amount"),
|
||||||
"fieldname": "depreciated_amount",
|
"fieldname": "depreciated_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"options": "company:currency",
|
"options": "Company:company:default_currency",
|
||||||
"width": 100,
|
"width": 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -469,4 +477,11 @@ def get_columns(filters):
|
|||||||
"options": "Location",
|
"options": "Location",
|
||||||
"width": 100,
|
"width": 100,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": _("Company"),
|
||||||
|
"fieldname": "company",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Company",
|
||||||
|
"width": 120,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1127,10 +1127,17 @@ class TestPurchaseOrder(FrappeTestCase):
|
|||||||
po = create_purchase_order()
|
po = create_purchase_order()
|
||||||
self.assertEqual(frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Not Initiated")
|
self.assertEqual(frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Not Initiated")
|
||||||
|
|
||||||
pr = make_payment_request(dt=po.doctype, dn=po.name, submit_doc=True, return_doc=True)
|
pr = make_payment_request(
|
||||||
|
dt=po.doctype, dn=po.name, submit_doc=True, return_doc=True, payment_request_type="Outward"
|
||||||
|
)
|
||||||
|
|
||||||
|
po.reload()
|
||||||
self.assertEqual(frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Initiated")
|
self.assertEqual(frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Initiated")
|
||||||
|
|
||||||
pe = get_payment_entry(po.doctype, po.name).save().submit()
|
pe = get_payment_entry(po.doctype, po.name).save().submit()
|
||||||
|
|
||||||
|
pr.reload()
|
||||||
|
self.assertEqual(pr.status, "Paid")
|
||||||
self.assertEqual(frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Fully Paid")
|
self.assertEqual(frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Fully Paid")
|
||||||
|
|
||||||
pe.reload()
|
pe.reload()
|
||||||
|
|||||||
@@ -22,9 +22,13 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
|
|||||||
this.frm.set_value("valid_till", frappe.datetime.add_months(this.frm.doc.transaction_date, 1));
|
this.frm.set_value("valid_till", frappe.datetime.add_months(this.frm.doc.transaction_date, 1));
|
||||||
}
|
}
|
||||||
if (this.frm.doc.docstatus === 1) {
|
if (this.frm.doc.docstatus === 1) {
|
||||||
this.frm.add_custom_button(__("Purchase Order"), this.make_purchase_order, __("Create"));
|
this.frm.add_custom_button(
|
||||||
|
__("Purchase Order"),
|
||||||
|
this.make_purchase_order.bind(this),
|
||||||
|
__("Create")
|
||||||
|
);
|
||||||
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
|
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||||
this.frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create"));
|
this.frm.add_custom_button(__("Quotation"), this.make_quotation.bind(this), __("Create"));
|
||||||
} else if (this.frm.doc.docstatus === 0) {
|
} else if (this.frm.doc.docstatus === 0) {
|
||||||
this.frm.add_custom_button(
|
this.frm.add_custom_button(
|
||||||
__("Material Request"),
|
__("Material Request"),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, bold, qb, throw
|
from frappe import _, bold, qb, throw
|
||||||
@@ -1031,10 +1032,10 @@ class AccountsController(TransactionBase):
|
|||||||
"transaction_currency": self.get("currency") or self.company_currency,
|
"transaction_currency": self.get("currency") or self.company_currency,
|
||||||
"transaction_exchange_rate": self.get("conversion_rate", 1),
|
"transaction_exchange_rate": self.get("conversion_rate", 1),
|
||||||
"debit_in_transaction_currency": self.get_value_in_transaction_currency(
|
"debit_in_transaction_currency": self.get_value_in_transaction_currency(
|
||||||
account_currency, args, "debit"
|
account_currency, gl_dict, "debit"
|
||||||
),
|
),
|
||||||
"credit_in_transaction_currency": self.get_value_in_transaction_currency(
|
"credit_in_transaction_currency": self.get_value_in_transaction_currency(
|
||||||
account_currency, args, "credit"
|
account_currency, gl_dict, "credit"
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1066,11 +1067,11 @@ class AccountsController(TransactionBase):
|
|||||||
return "Debit Note"
|
return "Debit Note"
|
||||||
return self.doctype
|
return self.doctype
|
||||||
|
|
||||||
def get_value_in_transaction_currency(self, account_currency, args, field):
|
def get_value_in_transaction_currency(self, account_currency, gl_dict, field):
|
||||||
if account_currency == self.get("currency"):
|
if account_currency == self.get("currency"):
|
||||||
return args.get(field + "_in_account_currency")
|
return gl_dict.get(field + "_in_account_currency")
|
||||||
else:
|
else:
|
||||||
return flt(args.get(field, 0) / self.get("conversion_rate", 1))
|
return flt(gl_dict.get(field, 0) / self.get("conversion_rate", 1))
|
||||||
|
|
||||||
def validate_zero_qty_for_return_invoices_with_stock(self):
|
def validate_zero_qty_for_return_invoices_with_stock(self):
|
||||||
rows = []
|
rows = []
|
||||||
@@ -1924,32 +1925,43 @@ class AccountsController(TransactionBase):
|
|||||||
|
|
||||||
self.db_set("advance_paid", advance_paid)
|
self.db_set("advance_paid", advance_paid)
|
||||||
|
|
||||||
self.set_advance_payment_status(advance_paid, order_total)
|
self.set_advance_payment_status()
|
||||||
|
|
||||||
def set_advance_payment_status(self, advance_paid: float | None = None, order_total: float | None = None):
|
def set_advance_payment_status(self):
|
||||||
new_status = None
|
new_status = None
|
||||||
# if money is paid set the paid states
|
|
||||||
if advance_paid:
|
|
||||||
new_status = "Partially Paid" if advance_paid < order_total else "Fully Paid"
|
|
||||||
|
|
||||||
if not new_status:
|
stati = frappe.get_list(
|
||||||
prs = frappe.db.count(
|
"Payment Request",
|
||||||
"Payment Request",
|
{
|
||||||
{
|
"reference_doctype": self.doctype,
|
||||||
"reference_doctype": self.doctype,
|
"reference_name": self.name,
|
||||||
"reference_name": self.name,
|
"docstatus": 1,
|
||||||
"docstatus": 1,
|
},
|
||||||
},
|
pluck="status",
|
||||||
)
|
)
|
||||||
if self.doctype in frappe.get_hooks("advance_payment_receivable_doctypes"):
|
if self.doctype in frappe.get_hooks("advance_payment_receivable_doctypes"):
|
||||||
new_status = "Requested" if prs else "Not Requested"
|
if not stati:
|
||||||
if self.doctype in frappe.get_hooks("advance_payment_payable_doctypes"):
|
new_status = "Not Requested"
|
||||||
new_status = "Initiated" if prs else "Not Initiated"
|
elif "Requested" in stati or "Failed" in stati:
|
||||||
|
new_status = "Requested"
|
||||||
|
elif "Partially Paid" in stati:
|
||||||
|
new_status = "Partially Paid"
|
||||||
|
elif "Paid" in stati:
|
||||||
|
new_status = "Fully Paid"
|
||||||
|
if self.doctype in frappe.get_hooks("advance_payment_payable_doctypes"):
|
||||||
|
if not stati:
|
||||||
|
new_status = "Not Initiated"
|
||||||
|
elif "Initiated" in stati or "Failed" in stati or "Payment Ordered" in stati:
|
||||||
|
new_status = "Initiated"
|
||||||
|
elif "Partially Paid" in stati:
|
||||||
|
new_status = "Partially Paid"
|
||||||
|
elif "Paid" in stati:
|
||||||
|
new_status = "Fully Paid"
|
||||||
|
|
||||||
if new_status == self.advance_payment_status:
|
if new_status == self.advance_payment_status:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.db_set("advance_payment_status", new_status)
|
self.db_set("advance_payment_status", new_status, update_modified=False)
|
||||||
self.set_status(update=True)
|
self.set_status(update=True)
|
||||||
self.notify_update()
|
self.notify_update()
|
||||||
|
|
||||||
@@ -2078,21 +2090,26 @@ class AccountsController(TransactionBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def group_similar_items(self):
|
def group_similar_items(self):
|
||||||
group_item_qty = {}
|
grouped_items = {}
|
||||||
group_item_amount = {}
|
|
||||||
# to update serial number in print
|
# to update serial number in print
|
||||||
count = 0
|
count = 0
|
||||||
|
|
||||||
|
fields_to_group = frappe.get_hooks("fields_for_group_similar_items")
|
||||||
|
fields_to_group = set(fields_to_group)
|
||||||
|
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
group_item_qty[item.item_code] = group_item_qty.get(item.item_code, 0) + item.qty
|
item_values = grouped_items.setdefault(item.item_code, defaultdict(int))
|
||||||
group_item_amount[item.item_code] = group_item_amount.get(item.item_code, 0) + item.amount
|
|
||||||
|
for field in fields_to_group:
|
||||||
|
item_values[field] += item.get(field, 0)
|
||||||
|
|
||||||
duplicate_list = []
|
duplicate_list = []
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
if item.item_code in group_item_qty:
|
if item.item_code in grouped_items:
|
||||||
count += 1
|
count += 1
|
||||||
item.qty = group_item_qty[item.item_code]
|
|
||||||
item.amount = group_item_amount[item.item_code]
|
for field in fields_to_group:
|
||||||
|
item.set(field, grouped_items[item.item_code][field])
|
||||||
|
|
||||||
if item.qty:
|
if item.qty:
|
||||||
item.rate = flt(flt(item.amount) / flt(item.qty), item.precision("rate"))
|
item.rate = flt(flt(item.amount) / flt(item.qty), item.precision("rate"))
|
||||||
@@ -2100,7 +2117,7 @@ class AccountsController(TransactionBase):
|
|||||||
item.rate = 0
|
item.rate = 0
|
||||||
|
|
||||||
item.idx = count
|
item.idx = count
|
||||||
del group_item_qty[item.item_code]
|
del grouped_items[item.item_code]
|
||||||
else:
|
else:
|
||||||
duplicate_list.append(item)
|
duplicate_list.append(item)
|
||||||
for item in duplicate_list:
|
for item in duplicate_list:
|
||||||
@@ -3510,6 +3527,37 @@ def check_if_child_table_updated(child_table_before_update, child_table_after_up
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def merge_taxes(source_taxes, target_doc):
|
||||||
|
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
|
||||||
|
update_item_wise_tax_detail,
|
||||||
|
)
|
||||||
|
|
||||||
|
existing_taxes = target_doc.get("taxes") or []
|
||||||
|
idx = 1
|
||||||
|
for tax in source_taxes:
|
||||||
|
found = False
|
||||||
|
for t in existing_taxes:
|
||||||
|
if t.account_head == tax.account_head and t.cost_center == tax.cost_center:
|
||||||
|
t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount)
|
||||||
|
t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount)
|
||||||
|
update_item_wise_tax_detail(t, tax)
|
||||||
|
found = True
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
tax.charge_type = "Actual"
|
||||||
|
tax.idx = idx
|
||||||
|
idx += 1
|
||||||
|
tax.included_in_print_rate = 0
|
||||||
|
tax.dont_recompute_tax = 1
|
||||||
|
tax.row_id = ""
|
||||||
|
tax.tax_amount = tax.tax_amount_after_discount_amount
|
||||||
|
tax.base_tax_amount = tax.base_tax_amount_after_discount_amount
|
||||||
|
tax.item_wise_tax_detail = tax.item_wise_tax_detail
|
||||||
|
existing_taxes.append(tax)
|
||||||
|
|
||||||
|
target_doc.set("taxes", existing_taxes)
|
||||||
|
|
||||||
|
|
||||||
@erpnext.allow_regional
|
@erpnext.allow_regional
|
||||||
def validate_regional(doc):
|
def validate_regional(doc):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -441,7 +441,7 @@ class SellingController(StockController):
|
|||||||
get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")
|
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
|
# 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 (
|
if (
|
||||||
not d.incoming_rate
|
not d.incoming_rate
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ class StockController(AccountsController):
|
|||||||
# remove extra whitespace and store one serial no on each line
|
# remove extra whitespace and store one serial no on each line
|
||||||
row.serial_no = clean_serial_no_string(row.serial_no)
|
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":
|
if self.get("_action") == "update_after_submit":
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -205,7 +205,7 @@ class StockController(AccountsController):
|
|||||||
"company": self.company,
|
"company": self.company,
|
||||||
"is_rejected": 1 if row.get("rejected_warehouse") else 0,
|
"is_rejected": 1 if row.get("rejected_warehouse") else 0,
|
||||||
"use_serial_batch_fields": row.use_serial_batch_fields,
|
"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"):
|
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)
|
message += _("Please adjust the qty or edit {0} to proceed.").format(rule_link)
|
||||||
return message
|
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(
|
args = frappe._dict(
|
||||||
{
|
{
|
||||||
"posting_date": self.posting_date,
|
"posting_date": self.posting_date,
|
||||||
@@ -1127,6 +1127,7 @@ class StockController(AccountsController):
|
|||||||
"voucher_type": self.doctype,
|
"voucher_type": self.doctype,
|
||||||
"voucher_no": self.name,
|
"voucher_no": self.name,
|
||||||
"company": self.company,
|
"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")
|
frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting")
|
||||||
)
|
)
|
||||||
if 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:
|
else:
|
||||||
create_repost_item_valuation_entry(args)
|
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.allow_zero_rate = args.allow_zero_rate
|
||||||
repost_entry.flags.ignore_links = True
|
repost_entry.flags.ignore_links = True
|
||||||
repost_entry.flags.ignore_permissions = True
|
repost_entry.flags.ignore_permissions = True
|
||||||
|
repost_entry.via_landed_cost_voucher = args.via_landed_cost_voucher
|
||||||
repost_entry.save()
|
repost_entry.save()
|
||||||
repost_entry.submit()
|
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."""
|
"""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)
|
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.allow_zero_rate = allow_zero_rate
|
||||||
repost_entry.flags.ignore_links = True
|
repost_entry.flags.ignore_links = True
|
||||||
repost_entry.flags.ignore_permissions = True
|
repost_entry.flags.ignore_permissions = True
|
||||||
|
repost_entry.via_landed_cost_voucher = via_landed_cost_voucher
|
||||||
repost_entry.submit()
|
repost_entry.submit()
|
||||||
repost_entries.append(repost_entry)
|
repost_entries.append(repost_entry)
|
||||||
|
|
||||||
|
|||||||
@@ -467,7 +467,16 @@ class calculate_taxes_and_totals:
|
|||||||
if tax.charge_type == "Actual":
|
if tax.charge_type == "Actual":
|
||||||
# distribute the tax amount proportionally to each item row
|
# distribute the tax amount proportionally to each item row
|
||||||
actual = flt(tax.tax_amount, tax.precision("tax_amount"))
|
actual = flt(tax.tax_amount, tax.precision("tax_amount"))
|
||||||
current_tax_amount = item.net_amount * actual / self.doc.net_total if self.doc.net_total else 0.0
|
|
||||||
|
if tax.get("is_tax_withholding_account") and item.meta.get_field("apply_tds"):
|
||||||
|
if not item.get("apply_tds") or not self.doc.tax_withholding_net_total:
|
||||||
|
current_tax_amount = 0.0
|
||||||
|
else:
|
||||||
|
current_tax_amount = item.net_amount * actual / self.doc.tax_withholding_net_total
|
||||||
|
else:
|
||||||
|
current_tax_amount = (
|
||||||
|
item.net_amount * actual / self.doc.net_total if self.doc.net_total else 0.0
|
||||||
|
)
|
||||||
|
|
||||||
elif tax.charge_type == "On Net Total":
|
elif tax.charge_type == "On Net Total":
|
||||||
current_tax_amount = (tax_rate / 100.0) * item.net_amount
|
current_tax_amount = (tax_rate / 100.0) * item.net_amount
|
||||||
@@ -1091,6 +1100,11 @@ def get_rounded_tax_amount(itemised_tax, precision):
|
|||||||
row["tax_amount"] = flt(row["tax_amount"], precision)
|
row["tax_amount"] = flt(row["tax_amount"], precision)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_rounding_tax_settings():
|
||||||
|
return frappe.db.get_single_value("Accounts Settings", "round_row_wise_tax")
|
||||||
|
|
||||||
|
|
||||||
class init_landed_taxes_and_totals:
|
class init_landed_taxes_and_totals:
|
||||||
def __init__(self, doc):
|
def __init__(self, doc):
|
||||||
self.doc = doc
|
self.doc = doc
|
||||||
|
|||||||
@@ -53,7 +53,8 @@ class TestAccountsController(FrappeTestCase):
|
|||||||
20 series - Sales Invoice against Journals
|
20 series - Sales Invoice against Journals
|
||||||
30 series - Sales Invoice against Credit Notes
|
30 series - Sales Invoice against Credit Notes
|
||||||
40 series - Company default Cost center is unset
|
40 series - Company default Cost center is unset
|
||||||
50 series = Journals against Journals
|
50 series - Journals against Journals
|
||||||
|
60 series - Journals against Payment Entries
|
||||||
90 series - Dimension inheritence
|
90 series - Dimension inheritence
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -1538,3 +1539,70 @@ class TestAccountsController(FrappeTestCase):
|
|||||||
exc_je_for_je = self.get_journals_for(journal_as_payment.doctype, journal_as_payment.name)
|
exc_je_for_je = self.get_journals_for(journal_as_payment.doctype, journal_as_payment.name)
|
||||||
self.assertEqual(exc_je_for_si, [])
|
self.assertEqual(exc_je_for_si, [])
|
||||||
self.assertEqual(exc_je_for_je, [])
|
self.assertEqual(exc_je_for_je, [])
|
||||||
|
|
||||||
|
def test_60_payment_entry_against_journal(self):
|
||||||
|
# Invoices
|
||||||
|
exc_rate1 = 75
|
||||||
|
exc_rate2 = 77
|
||||||
|
amount = 1
|
||||||
|
je1 = self.create_journal_entry(
|
||||||
|
acc1=self.debit_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 = "Customer"
|
||||||
|
je1.accounts[0].party = self.customer
|
||||||
|
je1 = je1.save().submit()
|
||||||
|
|
||||||
|
je2 = self.create_journal_entry(
|
||||||
|
acc1=self.debit_usd,
|
||||||
|
acc1_exc_rate=exc_rate2,
|
||||||
|
acc2=self.cash,
|
||||||
|
acc1_amount=amount,
|
||||||
|
acc2_amount=(amount * exc_rate2),
|
||||||
|
acc2_exc_rate=1,
|
||||||
|
)
|
||||||
|
je2.accounts[0].party_type = "Customer"
|
||||||
|
je2.accounts[0].party = self.customer
|
||||||
|
je2 = je2.save().submit()
|
||||||
|
|
||||||
|
# Payment
|
||||||
|
pe = self.create_payment_entry(amount=2, source_exc_rate=exc_rate1).save().submit()
|
||||||
|
|
||||||
|
pr = self.create_payment_reconciliation()
|
||||||
|
pr.receivable_payable_account = self.debit_usd
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
self.assertEqual(len(pr.invoices), 2)
|
||||||
|
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)
|
||||||
|
self.assert_ledger_outstanding(je2.doctype, je2.name, 0.0, 0.0)
|
||||||
|
|
||||||
|
# Exchange Gain/Loss Journal should've been created only for JE2
|
||||||
|
exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name)
|
||||||
|
exc_je_for_je2 = self.get_journals_for(je2.doctype, je2.name)
|
||||||
|
self.assertEqual(exc_je_for_je1, [])
|
||||||
|
self.assertEqual(len(exc_je_for_je2), 1)
|
||||||
|
|
||||||
|
# Cancel Payment
|
||||||
|
pe.reload()
|
||||||
|
pe.cancel()
|
||||||
|
|
||||||
|
self.assert_ledger_outstanding(je1.doctype, je1.name, (amount * exc_rate1), amount)
|
||||||
|
self.assert_ledger_outstanding(je2.doctype, je2.name, (amount * exc_rate2), amount)
|
||||||
|
|
||||||
|
# Exchange Gain/Loss Journal should've been cancelled
|
||||||
|
exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name)
|
||||||
|
exc_je_for_je2 = self.get_journals_for(je2.doctype, je2.name)
|
||||||
|
self.assertEqual(exc_je_for_je1, [])
|
||||||
|
self.assertEqual(exc_je_for_je2, [])
|
||||||
|
|||||||
@@ -349,7 +349,6 @@ doc_events = {
|
|||||||
"Payment Entry": {
|
"Payment Entry": {
|
||||||
"on_submit": [
|
"on_submit": [
|
||||||
"erpnext.regional.create_transaction_log",
|
"erpnext.regional.create_transaction_log",
|
||||||
"erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status",
|
|
||||||
"erpnext.accounts.doctype.dunning.dunning.resolve_dunning",
|
"erpnext.accounts.doctype.dunning.dunning.resolve_dunning",
|
||||||
],
|
],
|
||||||
"on_cancel": ["erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
|
"on_cancel": ["erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
|
||||||
@@ -435,6 +434,7 @@ scheduler_events = {
|
|||||||
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
|
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
|
||||||
"erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
|
"erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
|
||||||
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_daily",
|
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_daily",
|
||||||
|
"erpnext.accounts.utils.run_ledger_health_checks",
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
|
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
|
||||||
@@ -534,6 +534,7 @@ accounting_dimension_doctypes = [
|
|||||||
"Supplier Quotation Item",
|
"Supplier Quotation Item",
|
||||||
"Payment Reconciliation",
|
"Payment Reconciliation",
|
||||||
"Payment Reconciliation Allocation",
|
"Payment Reconciliation Allocation",
|
||||||
|
"Payment Request",
|
||||||
]
|
]
|
||||||
|
|
||||||
get_matching_queries = (
|
get_matching_queries = (
|
||||||
@@ -635,3 +636,5 @@ default_log_clearing_doctypes = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export_python_type_annotations = True
|
export_python_type_annotations = True
|
||||||
|
|
||||||
|
fields_for_group_similar_items = ["qty", "amount"]
|
||||||
|
|||||||
6664
erpnext/locale/ar.po
6664
erpnext/locale/ar.po
File diff suppressed because it is too large
Load Diff
83599
erpnext/locale/bs.po
Normal file
83599
erpnext/locale/bs.po
Normal file
File diff suppressed because it is too large
Load Diff
6908
erpnext/locale/de.po
6908
erpnext/locale/de.po
File diff suppressed because it is too large
Load Diff
6664
erpnext/locale/eo.po
6664
erpnext/locale/eo.po
File diff suppressed because it is too large
Load Diff
6812
erpnext/locale/es.po
6812
erpnext/locale/es.po
File diff suppressed because it is too large
Load Diff
7095
erpnext/locale/fa.po
7095
erpnext/locale/fa.po
File diff suppressed because it is too large
Load Diff
6664
erpnext/locale/fr.po
6664
erpnext/locale/fr.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
6700
erpnext/locale/tr.po
6700
erpnext/locale/tr.po
File diff suppressed because it is too large
Load Diff
@@ -238,7 +238,7 @@
|
|||||||
"fieldname": "rm_cost_as_per",
|
"fieldname": "rm_cost_as_per",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Rate Of Materials Based On",
|
"label": "Rate Of Materials Based On",
|
||||||
"options": "Valuation Rate\nLast Purchase Rate\nPrice List\nManual"
|
"options": "Valuation Rate\nLast Purchase Rate\nPrice List"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
@@ -637,7 +637,7 @@
|
|||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:06:40.214929",
|
"modified": "2024-04-02 16:22:47.518411",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "BOM",
|
"name": "BOM",
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ class BOM(WebsiteGenerator):
|
|||||||
quality_inspection_template: DF.Link | None
|
quality_inspection_template: DF.Link | None
|
||||||
quantity: DF.Float
|
quantity: DF.Float
|
||||||
raw_material_cost: DF.Currency
|
raw_material_cost: DF.Currency
|
||||||
rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List", "Manual"]
|
rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List"]
|
||||||
route: DF.SmallText | None
|
route: DF.SmallText | None
|
||||||
routing: DF.Link | None
|
routing: DF.Link | None
|
||||||
scrap_items: DF.Table[BOMScrapItem]
|
scrap_items: DF.Table[BOMScrapItem]
|
||||||
@@ -737,6 +737,7 @@ class BOM(WebsiteGenerator):
|
|||||||
|
|
||||||
def calculate_rm_cost(self, save=False):
|
def calculate_rm_cost(self, save=False):
|
||||||
"""Fetch RM rate as per today's valuation rate and calculate totals"""
|
"""Fetch RM rate as per today's valuation rate and calculate totals"""
|
||||||
|
|
||||||
total_rm_cost = 0
|
total_rm_cost = 0
|
||||||
base_total_rm_cost = 0
|
base_total_rm_cost = 0
|
||||||
|
|
||||||
@@ -745,7 +746,7 @@ class BOM(WebsiteGenerator):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
old_rate = d.rate
|
old_rate = d.rate
|
||||||
if self.rm_cost_as_per != "Manual":
|
if not self.bom_creator:
|
||||||
d.rate = self.get_rm_rate(
|
d.rate = self.get_rm_rate(
|
||||||
{
|
{
|
||||||
"company": self.company,
|
"company": self.company,
|
||||||
@@ -1017,8 +1018,6 @@ def get_bom_item_rate(args, bom_doc):
|
|||||||
item_doc = frappe.get_cached_doc("Item", args.get("item_code"))
|
item_doc = frappe.get_cached_doc("Item", args.get("item_code"))
|
||||||
price_list_data = get_price_list_rate(bom_args, item_doc)
|
price_list_data = get_price_list_rate(bom_args, item_doc)
|
||||||
rate = price_list_data.price_list_rate
|
rate = price_list_data.price_list_rate
|
||||||
elif bom_doc.rm_cost_as_per == "Manual":
|
|
||||||
return
|
|
||||||
|
|
||||||
return flt(rate)
|
return flt(rate)
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
"fieldname": "rm_cost_as_per",
|
"fieldname": "rm_cost_as_per",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Rate Of Materials Based On",
|
"label": "Rate Of Materials Based On",
|
||||||
"options": "Valuation Rate\nLast Purchase Rate\nPrice List\nManual",
|
"options": "Valuation Rate\nLast Purchase Rate\nPrice List",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -288,7 +288,7 @@
|
|||||||
"link_fieldname": "bom_creator"
|
"link_fieldname": "bom_creator"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-03-27 13:06:40.535884",
|
"modified": "2024-04-02 16:30:59.779190",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "BOM Creator",
|
"name": "BOM Creator",
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ class BOMCreator(Document):
|
|||||||
qty: DF.Float
|
qty: DF.Float
|
||||||
raw_material_cost: DF.Currency
|
raw_material_cost: DF.Currency
|
||||||
remarks: DF.TextEditor | None
|
remarks: DF.TextEditor | None
|
||||||
rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List", "Manual"]
|
rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List"]
|
||||||
set_rate_based_on_warehouse: DF.Check
|
set_rate_based_on_warehouse: DF.Check
|
||||||
status: DF.Literal["Draft", "Submitted", "In Progress", "Completed", "Failed", "Cancelled"]
|
status: DF.Literal["Draft", "Submitted", "In Progress", "Completed", "Failed", "Cancelled"]
|
||||||
uom: DF.Link | None
|
uom: DF.Link | None
|
||||||
@@ -141,9 +141,6 @@ class BOMCreator(Document):
|
|||||||
self.submit()
|
self.submit()
|
||||||
|
|
||||||
def set_rate_for_items(self):
|
def set_rate_for_items(self):
|
||||||
if self.rm_cost_as_per == "Manual":
|
|
||||||
return
|
|
||||||
|
|
||||||
amount = self.get_raw_material_cost()
|
amount = self.get_raw_material_cost()
|
||||||
self.raw_material_cost = amount
|
self.raw_material_cost = amount
|
||||||
|
|
||||||
@@ -239,6 +236,9 @@ class BOMCreator(Document):
|
|||||||
frappe._dict({"items": [], "bom_no": "", "fg_item_data": row}),
|
frappe._dict({"items": [], "bom_no": "", "fg_item_data": row}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not row.fg_reference_id and production_item_wise_rm.get((row.fg_item, row.fg_reference_id)):
|
||||||
|
frappe.throw(_("Please set Parent Row No for item {0}").format(row.fg_item))
|
||||||
|
|
||||||
production_item_wise_rm[(row.fg_item, row.fg_reference_id)]["items"].append(row)
|
production_item_wise_rm[(row.fg_item, row.fg_reference_id)]["items"].append(row)
|
||||||
|
|
||||||
reverse_tree = OrderedDict(reversed(list(production_item_wise_rm.items())))
|
reverse_tree = OrderedDict(reversed(list(production_item_wise_rm.items())))
|
||||||
@@ -282,7 +282,6 @@ class BOMCreator(Document):
|
|||||||
"allow_alternative_item": 1,
|
"allow_alternative_item": 1,
|
||||||
"bom_creator": self.name,
|
"bom_creator": self.name,
|
||||||
"bom_creator_item": bom_creator_item,
|
"bom_creator_item": bom_creator_item,
|
||||||
"rm_cost_as_per": "Manual",
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ class WorkstationDashboard {
|
|||||||
me.job_cards = [r.message];
|
me.job_cards = [r.message];
|
||||||
me.prepare_timer();
|
me.prepare_timer();
|
||||||
me.update_job_card_details();
|
me.update_job_card_details();
|
||||||
|
me.frm.reload_doc();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -229,6 +230,7 @@ class WorkstationDashboard {
|
|||||||
me.job_cards = [r.message];
|
me.job_cards = [r.message];
|
||||||
me.prepare_timer();
|
me.prepare_timer();
|
||||||
me.update_job_card_details();
|
me.update_job_card_details();
|
||||||
|
me.frm.reload_doc();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -357,6 +357,7 @@ erpnext.patches.v15_0.create_advance_payment_status
|
|||||||
erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes
|
erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes
|
||||||
erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
|
erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
|
||||||
erpnext.patches.v14_0.update_flag_for_return_invoices #2024-03-22
|
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
|
# below migration patch should always run last
|
||||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||||
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
|
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")
|
||||||
@@ -210,10 +210,12 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
frappe.db.get_single_value("Accounts Settings", "round_row_wise_tax")
|
frappe.call({
|
||||||
.then((round_row_wise_tax) => {
|
method: "erpnext.controllers.taxes_and_totals.get_rounding_tax_settings",
|
||||||
frappe.flags.round_row_wise_tax = round_row_wise_tax;
|
callback: function(r) {
|
||||||
})
|
frappe.flags.round_off_settings = r.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
determine_exclusive_rate() {
|
determine_exclusive_rate() {
|
||||||
|
|||||||
@@ -252,9 +252,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggle_enable_for_stock_uom(field) {
|
toggle_enable_for_stock_uom(field) {
|
||||||
frappe.db.get_single_value('Stock Settings', field)
|
frappe.call({
|
||||||
.then(value => {
|
method: 'erpnext.stock.doctype.stock_settings.stock_settings.get_enable_stock_uom_editing',
|
||||||
this.frm.fields_dict["items"].grid.toggle_enable("stock_qty", value);
|
callback: (r) => {
|
||||||
|
if (r.message) {
|
||||||
|
var value = r.message[field];
|
||||||
|
this.frm.fields_dict["items"].grid.toggle_enable("stock_qty", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -452,6 +452,9 @@ $.extend(erpnext.utils, {
|
|||||||
},
|
},
|
||||||
|
|
||||||
get_fiscal_year: function (date, with_dates = false, boolean = false) {
|
get_fiscal_year: function (date, with_dates = false, boolean = false) {
|
||||||
|
if (!frappe.boot.setup_complete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!date) {
|
if (!date) {
|
||||||
date = frappe.datetime.get_today();
|
date = frappe.datetime.get_today();
|
||||||
}
|
}
|
||||||
@@ -934,7 +937,7 @@ erpnext.utils.map_current_doc = function (opts) {
|
|||||||
|
|
||||||
if (opts.source_doctype) {
|
if (opts.source_doctype) {
|
||||||
let data_fields = [];
|
let data_fields = [];
|
||||||
if (opts.source_doctype == "Purchase Receipt") {
|
if (["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype)) {
|
||||||
data_fields.push({
|
data_fields.push({
|
||||||
fieldname: "merge_taxes",
|
fieldname: "merge_taxes",
|
||||||
fieldtype: "Check",
|
fieldtype: "Check",
|
||||||
@@ -960,7 +963,10 @@ erpnext.utils.map_current_doc = function (opts) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
opts.source_name = values;
|
opts.source_name = values;
|
||||||
if (opts.allow_child_item_selection || opts.source_doctype == "Purchase Receipt") {
|
if (
|
||||||
|
opts.allow_child_item_selection ||
|
||||||
|
["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype)
|
||||||
|
) {
|
||||||
// args contains filtered child docnames
|
// args contains filtered child docnames
|
||||||
opts.args = args;
|
opts.args = args;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -384,7 +384,6 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
|
|||||||
)
|
)
|
||||||
|
|
||||||
target.flags.ignore_permissions = ignore_permissions
|
target.flags.ignore_permissions = ignore_permissions
|
||||||
target.delivery_date = nowdate()
|
|
||||||
target.run_method("set_missing_values")
|
target.run_method("set_missing_values")
|
||||||
target.run_method("calculate_taxes_and_totals")
|
target.run_method("calculate_taxes_and_totals")
|
||||||
|
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ class TestQuotation(FrappeTestCase):
|
|||||||
|
|
||||||
sales_order.naming_series = "_T-Quotation-"
|
sales_order.naming_series = "_T-Quotation-"
|
||||||
sales_order.transaction_date = nowdate()
|
sales_order.transaction_date = nowdate()
|
||||||
|
sales_order.delivery_date = nowdate()
|
||||||
sales_order.insert()
|
sales_order.insert()
|
||||||
|
|
||||||
def test_make_sales_order_with_terms(self):
|
def test_make_sales_order_with_terms(self):
|
||||||
@@ -164,6 +165,7 @@ class TestQuotation(FrappeTestCase):
|
|||||||
|
|
||||||
sales_order.naming_series = "_T-Quotation-"
|
sales_order.naming_series = "_T-Quotation-"
|
||||||
sales_order.transaction_date = nowdate()
|
sales_order.transaction_date = nowdate()
|
||||||
|
sales_order.delivery_date = nowdate()
|
||||||
sales_order.insert()
|
sales_order.insert()
|
||||||
|
|
||||||
# Remove any unknown taxes if applied
|
# Remove any unknown taxes if applied
|
||||||
|
|||||||
@@ -111,16 +111,26 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (frm.doc.docstatus === 0) {
|
if (frm.doc.docstatus === 0) {
|
||||||
frappe.db.get_single_value("Stock Settings", "enable_stock_reservation").then((value) => {
|
frappe.call({
|
||||||
if (!value) {
|
method: "erpnext.selling.doctype.sales_order.sales_order.get_stock_reservation_status",
|
||||||
// If `Stock Reservation` is disabled in Stock Settings, set Reserve Stock to 0 and make the field read-only and hidden.
|
callback: function (r) {
|
||||||
frm.set_value("reserve_stock", 0);
|
if (!r.message) {
|
||||||
frm.set_df_property("reserve_stock", "read_only", 1);
|
frm.set_value("reserve_stock", 0);
|
||||||
frm.set_df_property("reserve_stock", "hidden", 1);
|
frm.set_df_property("reserve_stock", "read_only", 1);
|
||||||
frm.fields_dict.items.grid.update_docfield_property("reserve_stock", "hidden", 1);
|
frm.set_df_property("reserve_stock", "hidden", 1);
|
||||||
frm.fields_dict.items.grid.update_docfield_property("reserve_stock", "default", 0);
|
frm.fields_dict.items.grid.update_docfield_property("reserve_stock", "hidden", 1);
|
||||||
frm.fields_dict.items.grid.update_docfield_property("reserve_stock", "read_only", 1);
|
frm.fields_dict.items.grid.update_docfield_property(
|
||||||
}
|
"reserve_stock",
|
||||||
|
"default",
|
||||||
|
0
|
||||||
|
);
|
||||||
|
frm.fields_dict.items.grid.update_docfield_property(
|
||||||
|
"reserve_stock",
|
||||||
|
"read_only",
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,6 +288,7 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
label: __("Items to Reserve"),
|
label: __("Items to Reserve"),
|
||||||
allow_bulk_edit: false,
|
allow_bulk_edit: false,
|
||||||
cannot_add_rows: true,
|
cannot_add_rows: true,
|
||||||
|
cannot_delete_rows: true,
|
||||||
data: [],
|
data: [],
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
@@ -346,7 +357,7 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
],
|
],
|
||||||
primary_action_label: __("Reserve Stock"),
|
primary_action_label: __("Reserve Stock"),
|
||||||
primary_action: () => {
|
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) {
|
if (data.items && data.items.length > 0) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
@@ -363,9 +374,11 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
frm.reload_doc();
|
frm.reload_doc();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
dialog.hide();
|
dialog.hide();
|
||||||
|
} else {
|
||||||
|
frappe.msgprint(__("Please select items to reserve."));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -380,6 +393,7 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
|
|
||||||
if (unreserved_qty > 0) {
|
if (unreserved_qty > 0) {
|
||||||
dialog.fields_dict.items.df.data.push({
|
dialog.fields_dict.items.df.data.push({
|
||||||
|
__checked: 1,
|
||||||
sales_order_item: item.name,
|
sales_order_item: item.name,
|
||||||
item_code: item.item_code,
|
item_code: item.item_code,
|
||||||
warehouse: item.warehouse,
|
warehouse: item.warehouse,
|
||||||
@@ -404,6 +418,7 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
label: __("Reserved Stock"),
|
label: __("Reserved Stock"),
|
||||||
allow_bulk_edit: false,
|
allow_bulk_edit: false,
|
||||||
cannot_add_rows: true,
|
cannot_add_rows: true,
|
||||||
|
cannot_delete_rows: true,
|
||||||
in_place_edit: true,
|
in_place_edit: true,
|
||||||
data: [],
|
data: [],
|
||||||
fields: [
|
fields: [
|
||||||
@@ -447,7 +462,7 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
],
|
],
|
||||||
primary_action_label: __("Unreserve Stock"),
|
primary_action_label: __("Unreserve Stock"),
|
||||||
primary_action: () => {
|
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) {
|
if (data.sr_entries && data.sr_entries.length > 0) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
@@ -463,9 +478,11 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
frm.reload_doc();
|
frm.reload_doc();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
dialog.hide();
|
dialog.hide();
|
||||||
|
} else {
|
||||||
|
frappe.msgprint(__("Please select items to unreserve."));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1724,3 +1724,8 @@ def get_work_order_items(sales_order, for_raw_material_request=0):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_stock_reservation_status():
|
||||||
|
return frappe.db.get_single_value("Stock Settings", "enable_stock_reservation")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
import frappe.permissions
|
import frappe.permissions
|
||||||
@@ -1956,10 +1957,48 @@ class TestSalesOrder(FrappeTestCase):
|
|||||||
self.assertEqual(so.items[0].rate, scenario.get("expected_rate"))
|
self.assertEqual(so.items[0].rate, scenario.get("expected_rate"))
|
||||||
self.assertEqual(so.packed_items[0].rate, scenario.get("expected_rate"))
|
self.assertEqual(so.packed_items[0].rate, scenario.get("expected_rate"))
|
||||||
|
|
||||||
def test_sales_order_advance_payment_status(self):
|
@patch(
|
||||||
|
# this also shadows one (1) call to _get_payment_gateway_controller
|
||||||
|
"erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.get_payment_url",
|
||||||
|
return_value=None,
|
||||||
|
)
|
||||||
|
def test_sales_order_advance_payment_status(self, mocked_get_payment_url):
|
||||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||||
|
|
||||||
|
# Flow progressing to SI with payment entries "moved" from SO to SI
|
||||||
|
so = make_sales_order(qty=1, rate=100, do_not_submit=True)
|
||||||
|
# no-op; for optical consistency with how a webshop SO would look like
|
||||||
|
so.order_type = "Shopping Cart"
|
||||||
|
so.submit()
|
||||||
|
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested")
|
||||||
|
|
||||||
|
pr = make_payment_request(
|
||||||
|
dt=so.doctype, dn=so.name, order_type="Shopping Cart", submit_doc=True, return_doc=True
|
||||||
|
)
|
||||||
|
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Requested")
|
||||||
|
|
||||||
|
pe = pr.set_as_paid()
|
||||||
|
pr.reload() # status updated
|
||||||
|
pe.reload() # references moved to Sales Invoice
|
||||||
|
self.assertEqual(pr.status, "Paid")
|
||||||
|
self.assertEqual(pe.references[0].reference_doctype, "Sales Invoice")
|
||||||
|
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Fully Paid")
|
||||||
|
|
||||||
|
pe.cancel()
|
||||||
|
pr.reload()
|
||||||
|
self.assertEqual(pr.status, "Paid") # TODO: this might be a bug
|
||||||
|
so.reload() # reload
|
||||||
|
# regardless, since the references have already "handed-over" to SI,
|
||||||
|
# the SO keeps its historical state at the time of hand over
|
||||||
|
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Fully Paid")
|
||||||
|
|
||||||
|
pr.cancel()
|
||||||
|
self.assertEqual(
|
||||||
|
frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested"
|
||||||
|
) # TODO: this might be a bug; handover has happened
|
||||||
|
|
||||||
|
# Flow NOT progressing to SI with payment entries NOT "moved"
|
||||||
so = make_sales_order(qty=1, rate=100)
|
so = make_sales_order(qty=1, rate=100)
|
||||||
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested")
|
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested")
|
||||||
|
|
||||||
@@ -1971,11 +2010,15 @@ class TestSalesOrder(FrappeTestCase):
|
|||||||
|
|
||||||
pe.reload()
|
pe.reload()
|
||||||
pe.cancel()
|
pe.cancel()
|
||||||
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Requested")
|
self.assertEqual(
|
||||||
|
frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Requested"
|
||||||
|
) # here: reset
|
||||||
|
|
||||||
pr.reload()
|
pr.reload()
|
||||||
pr.cancel()
|
pr.cancel()
|
||||||
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested")
|
self.assertEqual(
|
||||||
|
frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested"
|
||||||
|
) # here: reset
|
||||||
|
|
||||||
def test_pick_list_without_rejected_materials(self):
|
def test_pick_list_without_rejected_materials(self):
|
||||||
serial_and_batch_item = make_item(
|
serial_and_batch_item = make_item(
|
||||||
|
|||||||
@@ -547,6 +547,8 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
|
|
||||||
async on_cart_update(args) {
|
async on_cart_update(args) {
|
||||||
frappe.dom.freeze();
|
frappe.dom.freeze();
|
||||||
|
if (this.frm.doc.set_warehouse != this.settings.warehouse)
|
||||||
|
this.frm.doc.set_warehouse = this.settings.warehouse;
|
||||||
let item_row = undefined;
|
let item_row = undefined;
|
||||||
try {
|
try {
|
||||||
let { field, value, item } = args;
|
let { field, value, item } = args;
|
||||||
|
|||||||
@@ -142,6 +142,11 @@ def add_company_to_session_defaults():
|
|||||||
def add_standard_navbar_items():
|
def add_standard_navbar_items():
|
||||||
navbar_settings = frappe.get_single("Navbar Settings")
|
navbar_settings = frappe.get_single("Navbar Settings")
|
||||||
|
|
||||||
|
# Translatable strings for below navbar items
|
||||||
|
__ = _("Documentation")
|
||||||
|
__ = _("User Forum")
|
||||||
|
__ = _("Report an Issue")
|
||||||
|
|
||||||
erpnext_navbar_items = [
|
erpnext_navbar_items = [
|
||||||
{
|
{
|
||||||
"item_label": "Documentation",
|
"item_label": "Documentation",
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class ClosingStockBalance(Document):
|
|||||||
& (
|
& (
|
||||||
(table.from_date.between(self.from_date, self.to_date))
|
(table.from_date.between(self.from_date, self.to_date))
|
||||||
| (table.to_date.between(self.from_date, self.to_date))
|
| (table.to_date.between(self.from_date, self.to_date))
|
||||||
| (table.from_date >= self.from_date and table.to_date >= self.to_date)
|
| ((table.from_date >= self.from_date) & (table.to_date >= self.to_date))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from frappe.model.mapper import get_mapped_doc
|
|||||||
from frappe.model.utils import get_fetch_values
|
from frappe.model.utils import get_fetch_values
|
||||||
from frappe.utils import cint, flt
|
from frappe.utils import cint, flt
|
||||||
|
|
||||||
from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
from erpnext.controllers.accounts_controller import get_taxes_and_charges, merge_taxes
|
||||||
from erpnext.controllers.selling_controller import SellingController
|
from erpnext.controllers.selling_controller import SellingController
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no
|
from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no
|
||||||
|
|
||||||
@@ -960,7 +960,7 @@ def get_returned_qty_map(delivery_note):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_sales_invoice(source_name, target_doc=None):
|
def make_sales_invoice(source_name, target_doc=None, args=None):
|
||||||
doc = frappe.get_doc("Delivery Note", source_name)
|
doc = frappe.get_doc("Delivery Note", source_name)
|
||||||
|
|
||||||
to_make_invoice_qty_map = {}
|
to_make_invoice_qty_map = {}
|
||||||
@@ -974,6 +974,9 @@ def make_sales_invoice(source_name, target_doc=None):
|
|||||||
if len(target.get("items")) == 0:
|
if len(target.get("items")) == 0:
|
||||||
frappe.throw(_("All these items have already been Invoiced/Returned"))
|
frappe.throw(_("All these items have already been Invoiced/Returned"))
|
||||||
|
|
||||||
|
if args and args.get("merge_taxes"):
|
||||||
|
merge_taxes(source.get("taxes") or [], target)
|
||||||
|
|
||||||
target.run_method("calculate_taxes_and_totals")
|
target.run_method("calculate_taxes_and_totals")
|
||||||
|
|
||||||
# set company address
|
# set company address
|
||||||
@@ -1038,7 +1041,11 @@ def make_sales_invoice(source_name, target_doc=None):
|
|||||||
if not doc.get("is_return")
|
if not doc.get("is_return")
|
||||||
else get_pending_qty(d) > 0,
|
else get_pending_qty(d) > 0,
|
||||||
},
|
},
|
||||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
|
"Sales Taxes and Charges": {
|
||||||
|
"doctype": "Sales Taxes and Charges",
|
||||||
|
"add_if_empty": True,
|
||||||
|
"ignore": args.get("merge_taxes") if args else 0,
|
||||||
|
},
|
||||||
"Sales Team": {
|
"Sales Team": {
|
||||||
"doctype": "Sales Team",
|
"doctype": "Sales Team",
|
||||||
"field_map": {"incentives": "incentives"},
|
"field_map": {"incentives": "incentives"},
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ test_ignore = ["BOM"]
|
|||||||
test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"]
|
test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"]
|
||||||
|
|
||||||
|
|
||||||
def make_item(item_code=None, properties=None, uoms=None):
|
def make_item(item_code=None, properties=None, uoms=None, barcode=None):
|
||||||
if not item_code:
|
if not item_code:
|
||||||
item_code = frappe.generate_hash(length=16)
|
item_code = frappe.generate_hash(length=16)
|
||||||
|
|
||||||
@@ -61,6 +61,14 @@ def make_item(item_code=None, properties=None, uoms=None):
|
|||||||
for uom in uoms:
|
for uom in uoms:
|
||||||
item.append("uoms", uom)
|
item.append("uoms", uom)
|
||||||
|
|
||||||
|
if barcode:
|
||||||
|
item.append(
|
||||||
|
"barcodes",
|
||||||
|
{
|
||||||
|
"barcode": barcode,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
item.insert()
|
item.insert()
|
||||||
|
|
||||||
return item
|
return item
|
||||||
|
|||||||
@@ -250,9 +250,10 @@ class LandedCostVoucher(Document):
|
|||||||
|
|
||||||
# update stock & gl entries for submit state of PR
|
# update stock & gl entries for submit state of PR
|
||||||
doc.docstatus = 1
|
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.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True)
|
||||||
doc.make_gl_entries()
|
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):
|
def validate_asset_qty_and_status(self, receipt_document_type, receipt_document):
|
||||||
for item in self.get("items"):
|
for item in self.get("items"):
|
||||||
|
|||||||
@@ -596,6 +596,356 @@ class TestLandedCostVoucher(FrappeTestCase):
|
|||||||
lcv.cancel()
|
lcv.cancel()
|
||||||
pr.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):
|
def make_landed_cost_voucher(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"parent_warehouse",
|
"parent_warehouse",
|
||||||
"consider_rejected_warehouses",
|
"consider_rejected_warehouses",
|
||||||
"get_item_locations",
|
"get_item_locations",
|
||||||
|
"pick_manually",
|
||||||
"section_break_6",
|
"section_break_6",
|
||||||
"scan_barcode",
|
"scan_barcode",
|
||||||
"column_break_13",
|
"column_break_13",
|
||||||
@@ -192,11 +193,18 @@
|
|||||||
"fieldname": "consider_rejected_warehouses",
|
"fieldname": "consider_rejected_warehouses",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Consider Rejected Warehouses"
|
"label": "Consider Rejected Warehouses"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "If enabled then system won't override the picked qty / batches / serial numbers.",
|
||||||
|
"fieldname": "pick_manually",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Pick Manually"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:13.177072",
|
"modified": "2024-03-27 22:49:16.954637",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Pick List",
|
"name": "Pick List",
|
||||||
@@ -264,7 +272,7 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"sort_field": "creation",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from frappe.model.mapper import map_child_doc
|
|||||||
from frappe.query_builder import Case
|
from frappe.query_builder import Case
|
||||||
from frappe.query_builder.custom import GROUP_CONCAT
|
from frappe.query_builder.custom import GROUP_CONCAT
|
||||||
from frappe.query_builder.functions import Coalesce, Locate, Replace, Sum
|
from frappe.query_builder.functions import Coalesce, Locate, Replace, Sum
|
||||||
from frappe.utils import ceil, cint, floor, flt
|
from frappe.utils import ceil, cint, floor, flt, get_link_to_form
|
||||||
from frappe.utils.nestedset import get_descendants_of
|
from frappe.utils.nestedset import get_descendants_of
|
||||||
|
|
||||||
from erpnext.selling.doctype.sales_order.sales_order import (
|
from erpnext.selling.doctype.sales_order.sales_order import (
|
||||||
@@ -23,7 +23,11 @@ from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle impor
|
|||||||
get_picked_serial_nos,
|
get_picked_serial_nos,
|
||||||
)
|
)
|
||||||
from erpnext.stock.get_item_details import get_conversion_factor
|
from erpnext.stock.get_item_details import get_conversion_factor
|
||||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
from erpnext.stock.serial_batch_bundle import (
|
||||||
|
SerialBatchCreation,
|
||||||
|
get_batches_from_bundle,
|
||||||
|
get_serial_nos_from_bundle,
|
||||||
|
)
|
||||||
|
|
||||||
# TODO: Prioritize SO or WO group warehouse
|
# TODO: Prioritize SO or WO group warehouse
|
||||||
|
|
||||||
@@ -41,6 +45,7 @@ class PickList(Document):
|
|||||||
|
|
||||||
amended_from: DF.Link | None
|
amended_from: DF.Link | None
|
||||||
company: DF.Link
|
company: DF.Link
|
||||||
|
consider_rejected_warehouses: DF.Check
|
||||||
customer: DF.Link | None
|
customer: DF.Link | None
|
||||||
customer_name: DF.Data | None
|
customer_name: DF.Data | None
|
||||||
for_qty: DF.Float
|
for_qty: DF.Float
|
||||||
@@ -49,6 +54,7 @@ class PickList(Document):
|
|||||||
material_request: DF.Link | None
|
material_request: DF.Link | None
|
||||||
naming_series: DF.Literal["STO-PICK-.YYYY.-"]
|
naming_series: DF.Literal["STO-PICK-.YYYY.-"]
|
||||||
parent_warehouse: DF.Link | None
|
parent_warehouse: DF.Link | None
|
||||||
|
pick_manually: DF.Check
|
||||||
prompt_qty: DF.Check
|
prompt_qty: DF.Check
|
||||||
purpose: DF.Literal["Material Transfer for Manufacture", "Material Transfer", "Delivery"]
|
purpose: DF.Literal["Material Transfer for Manufacture", "Material Transfer", "Delivery"]
|
||||||
scan_barcode: DF.Data | None
|
scan_barcode: DF.Data | None
|
||||||
@@ -70,7 +76,8 @@ class PickList(Document):
|
|||||||
|
|
||||||
def before_save(self):
|
def before_save(self):
|
||||||
self.update_status()
|
self.update_status()
|
||||||
self.set_item_locations()
|
if not self.pick_manually:
|
||||||
|
self.set_item_locations()
|
||||||
|
|
||||||
if self.get("locations"):
|
if self.get("locations"):
|
||||||
self.validate_sales_order_percentage()
|
self.validate_sales_order_percentage()
|
||||||
@@ -198,10 +205,11 @@ class PickList(Document):
|
|||||||
row.db_set("serial_and_batch_bundle", None)
|
row.db_set("serial_and_batch_bundle", None)
|
||||||
|
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
self.linked_serial_and_batch_bundle()
|
if self.get("locations"):
|
||||||
|
self.linked_serial_and_batch_bundle()
|
||||||
|
|
||||||
def linked_serial_and_batch_bundle(self):
|
def linked_serial_and_batch_bundle(self):
|
||||||
for row in self.locations:
|
for row in self.get("locations"):
|
||||||
if row.serial_and_batch_bundle:
|
if row.serial_and_batch_bundle:
|
||||||
frappe.get_doc(
|
frappe.get_doc(
|
||||||
"Serial and Batch Bundle", row.serial_and_batch_bundle
|
"Serial and Batch Bundle", row.serial_and_batch_bundle
|
||||||
@@ -510,55 +518,82 @@ class PickList(Document):
|
|||||||
def get_picked_items_details(self, items):
|
def get_picked_items_details(self, items):
|
||||||
picked_items = frappe._dict()
|
picked_items = frappe._dict()
|
||||||
|
|
||||||
if items:
|
if not items:
|
||||||
pi = frappe.qb.DocType("Pick List")
|
return picked_items
|
||||||
pi_item = frappe.qb.DocType("Pick List Item")
|
|
||||||
query = (
|
|
||||||
frappe.qb.from_(pi)
|
|
||||||
.inner_join(pi_item)
|
|
||||||
.on(pi.name == pi_item.parent)
|
|
||||||
.select(
|
|
||||||
pi_item.item_code,
|
|
||||||
pi_item.warehouse,
|
|
||||||
pi_item.batch_no,
|
|
||||||
pi_item.serial_and_batch_bundle,
|
|
||||||
Sum(Case().when(pi_item.picked_qty > 0, pi_item.picked_qty).else_(pi_item.stock_qty)).as_(
|
|
||||||
"picked_qty"
|
|
||||||
),
|
|
||||||
Replace(GROUP_CONCAT(pi_item.serial_no), ",", "\n").as_("serial_no"),
|
|
||||||
)
|
|
||||||
.where(
|
|
||||||
(pi_item.item_code.isin([x.item_code for x in items]))
|
|
||||||
& ((pi_item.picked_qty > 0) | (pi_item.stock_qty > 0))
|
|
||||||
& (pi.status != "Completed")
|
|
||||||
& (pi.status != "Cancelled")
|
|
||||||
& (pi_item.docstatus != 2)
|
|
||||||
)
|
|
||||||
.groupby(
|
|
||||||
pi_item.item_code,
|
|
||||||
pi_item.warehouse,
|
|
||||||
pi_item.batch_no,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.name:
|
items_data = self._get_pick_list_items(items)
|
||||||
query = query.where(pi_item.parent != self.name)
|
|
||||||
|
|
||||||
items_data = query.run(as_dict=True)
|
for item_data in items_data:
|
||||||
|
key = (item_data.warehouse, item_data.batch_no) if item_data.batch_no else item_data.warehouse
|
||||||
|
serial_no = [x for x in item_data.serial_no.split("\n") if x] if item_data.serial_no else None
|
||||||
|
|
||||||
for item_data in items_data:
|
if item_data.serial_and_batch_bundle:
|
||||||
key = (item_data.warehouse, item_data.batch_no) if item_data.batch_no else item_data.warehouse
|
if not serial_no:
|
||||||
serial_no = [x for x in item_data.serial_no.split("\n") if x] if item_data.serial_no else None
|
serial_no = get_serial_nos_from_bundle(item_data.serial_and_batch_bundle)
|
||||||
data = {"picked_qty": item_data.picked_qty}
|
|
||||||
if serial_no:
|
if not item_data.batch_no and not serial_no:
|
||||||
data["serial_no"] = serial_no
|
bundle_batches = get_batches_from_bundle(item_data.serial_and_batch_bundle)
|
||||||
if item_data.item_code not in picked_items:
|
for batch_no, batch_qty in bundle_batches.items():
|
||||||
picked_items[item_data.item_code] = {key: data}
|
batch_qty = abs(batch_qty)
|
||||||
else:
|
|
||||||
picked_items[item_data.item_code][key] = data
|
key = (item_data.warehouse, batch_no)
|
||||||
|
if item_data.item_code not in picked_items:
|
||||||
|
picked_items[item_data.item_code] = {key: {"picked_qty": batch_qty}}
|
||||||
|
else:
|
||||||
|
picked_items[item_data.item_code][key]["picked_qty"] += batch_qty
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
|
if item_data.item_code not in picked_items:
|
||||||
|
picked_items[item_data.item_code] = {}
|
||||||
|
|
||||||
|
if key not in picked_items[item_data.item_code]:
|
||||||
|
picked_items[item_data.item_code][key] = frappe._dict(
|
||||||
|
{
|
||||||
|
"picked_qty": 0,
|
||||||
|
"serial_no": [],
|
||||||
|
"batch_no": item_data.batch_no or "",
|
||||||
|
"warehouse": item_data.warehouse,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
picked_items[item_data.item_code][key]["picked_qty"] += item_data.picked_qty
|
||||||
|
if serial_no:
|
||||||
|
picked_items[item_data.item_code][key]["serial_no"].extend(serial_no)
|
||||||
|
|
||||||
return picked_items
|
return picked_items
|
||||||
|
|
||||||
|
def _get_pick_list_items(self, items):
|
||||||
|
pi = frappe.qb.DocType("Pick List")
|
||||||
|
pi_item = frappe.qb.DocType("Pick List Item")
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(pi)
|
||||||
|
.inner_join(pi_item)
|
||||||
|
.on(pi.name == pi_item.parent)
|
||||||
|
.select(
|
||||||
|
pi_item.item_code,
|
||||||
|
pi_item.warehouse,
|
||||||
|
pi_item.batch_no,
|
||||||
|
pi_item.serial_and_batch_bundle,
|
||||||
|
pi_item.serial_no,
|
||||||
|
(Case().when(pi_item.picked_qty > 0, pi_item.picked_qty).else_(pi_item.stock_qty)).as_(
|
||||||
|
"picked_qty"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
(pi_item.item_code.isin([x.item_code for x in items]))
|
||||||
|
& ((pi_item.picked_qty > 0) | (pi_item.stock_qty > 0))
|
||||||
|
& (pi.status != "Completed")
|
||||||
|
& (pi.status != "Cancelled")
|
||||||
|
& (pi_item.docstatus != 2)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.name:
|
||||||
|
query = query.where(pi_item.parent != self.name)
|
||||||
|
|
||||||
|
return query.run(as_dict=True)
|
||||||
|
|
||||||
def _get_product_bundles(self) -> dict[str, str]:
|
def _get_product_bundles(self) -> dict[str, str]:
|
||||||
# Dict[so_item_row: item_code]
|
# Dict[so_item_row: item_code]
|
||||||
product_bundles = {}
|
product_bundles = {}
|
||||||
@@ -715,9 +750,7 @@ def get_available_item_locations(
|
|||||||
consider_rejected_warehouses=False,
|
consider_rejected_warehouses=False,
|
||||||
):
|
):
|
||||||
locations = []
|
locations = []
|
||||||
total_picked_qty = (
|
|
||||||
sum([v.get("picked_qty") for k, v in picked_item_details.items()]) if picked_item_details else 0
|
|
||||||
)
|
|
||||||
has_serial_no = frappe.get_cached_value("Item", item_code, "has_serial_no")
|
has_serial_no = frappe.get_cached_value("Item", item_code, "has_serial_no")
|
||||||
has_batch_no = frappe.get_cached_value("Item", item_code, "has_batch_no")
|
has_batch_no = frappe.get_cached_value("Item", item_code, "has_batch_no")
|
||||||
|
|
||||||
@@ -727,63 +760,90 @@ def get_available_item_locations(
|
|||||||
from_warehouses,
|
from_warehouses,
|
||||||
required_qty,
|
required_qty,
|
||||||
company,
|
company,
|
||||||
total_picked_qty,
|
|
||||||
consider_rejected_warehouses=consider_rejected_warehouses,
|
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||||
)
|
)
|
||||||
elif has_serial_no:
|
elif has_serial_no:
|
||||||
locations = get_available_item_locations_for_serialized_item(
|
locations = get_available_item_locations_for_serialized_item(
|
||||||
item_code,
|
item_code,
|
||||||
from_warehouses,
|
from_warehouses,
|
||||||
required_qty,
|
|
||||||
company,
|
company,
|
||||||
total_picked_qty,
|
|
||||||
consider_rejected_warehouses=consider_rejected_warehouses,
|
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||||
)
|
)
|
||||||
elif has_batch_no:
|
elif has_batch_no:
|
||||||
locations = get_available_item_locations_for_batched_item(
|
locations = get_available_item_locations_for_batched_item(
|
||||||
item_code,
|
item_code,
|
||||||
from_warehouses,
|
from_warehouses,
|
||||||
required_qty,
|
|
||||||
company,
|
|
||||||
total_picked_qty,
|
|
||||||
consider_rejected_warehouses=consider_rejected_warehouses,
|
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
locations = get_available_item_locations_for_other_item(
|
locations = get_available_item_locations_for_other_item(
|
||||||
item_code,
|
item_code,
|
||||||
from_warehouses,
|
from_warehouses,
|
||||||
required_qty,
|
|
||||||
company,
|
company,
|
||||||
total_picked_qty,
|
|
||||||
consider_rejected_warehouses=consider_rejected_warehouses,
|
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if picked_item_details:
|
||||||
|
locations = filter_locations_by_picked_materials(locations, picked_item_details)
|
||||||
|
|
||||||
|
if locations:
|
||||||
|
locations = get_locations_based_on_required_qty(locations, required_qty)
|
||||||
|
|
||||||
|
if not ignore_validation:
|
||||||
|
validate_picked_materials(item_code, required_qty, locations)
|
||||||
|
|
||||||
|
return locations
|
||||||
|
|
||||||
|
|
||||||
|
def get_locations_based_on_required_qty(locations, required_qty):
|
||||||
|
filtered_locations = []
|
||||||
|
|
||||||
|
for location in locations:
|
||||||
|
if location.qty >= required_qty:
|
||||||
|
location.qty = required_qty
|
||||||
|
filtered_locations.append(location)
|
||||||
|
break
|
||||||
|
|
||||||
|
required_qty -= location.qty
|
||||||
|
filtered_locations.append(location)
|
||||||
|
|
||||||
|
return filtered_locations
|
||||||
|
|
||||||
|
|
||||||
|
def validate_picked_materials(item_code, required_qty, locations):
|
||||||
|
for location in list(locations):
|
||||||
|
if location["qty"] < 0:
|
||||||
|
locations.remove(location)
|
||||||
|
|
||||||
total_qty_available = sum(location.get("qty") for location in locations)
|
total_qty_available = sum(location.get("qty") for location in locations)
|
||||||
remaining_qty = required_qty - total_qty_available
|
remaining_qty = required_qty - total_qty_available
|
||||||
|
|
||||||
if remaining_qty > 0 and not ignore_validation:
|
if remaining_qty > 0:
|
||||||
frappe.msgprint(
|
frappe.msgprint(
|
||||||
_("{0} units of Item {1} is not available.").format(
|
_("{0} units of Item {1} is picked in another Pick List.").format(
|
||||||
remaining_qty, frappe.get_desk_link("Item", item_code)
|
remaining_qty, get_link_to_form("Item", item_code)
|
||||||
),
|
),
|
||||||
title=_("Insufficient Stock"),
|
title=_("Already Picked"),
|
||||||
)
|
)
|
||||||
|
|
||||||
if picked_item_details:
|
|
||||||
for location in list(locations):
|
|
||||||
if location["qty"] < 0:
|
|
||||||
locations.remove(location)
|
|
||||||
|
|
||||||
total_qty_available = sum(location.get("qty") for location in locations)
|
def filter_locations_by_picked_materials(locations, picked_item_details) -> list[dict]:
|
||||||
remaining_qty = required_qty - total_qty_available
|
for row in locations:
|
||||||
|
key = row.warehouse
|
||||||
|
if row.batch_no:
|
||||||
|
key = (row.warehouse, row.batch_no)
|
||||||
|
|
||||||
if remaining_qty > 0 and not ignore_validation:
|
picked_qty = picked_item_details.get(key, {}).get("picked_qty", 0)
|
||||||
frappe.msgprint(
|
if not picked_qty:
|
||||||
_("{0} units of Item {1} is picked in another Pick List.").format(
|
continue
|
||||||
remaining_qty, frappe.get_desk_link("Item", item_code)
|
if picked_qty > row.qty:
|
||||||
),
|
row.qty = 0
|
||||||
title=_("Already Picked"),
|
picked_item_details[key]["picked_qty"] -= row.qty
|
||||||
)
|
else:
|
||||||
|
row.qty -= picked_qty
|
||||||
|
picked_item_details[key]["picked_qty"] = 0.0
|
||||||
|
if row.serial_nos:
|
||||||
|
row.serial_nos = list(set(row.serial_nos) - set(picked_item_details[key].get("serial_no")))
|
||||||
|
|
||||||
return locations
|
return locations
|
||||||
|
|
||||||
@@ -793,15 +853,12 @@ def get_available_item_locations_for_serial_and_batched_item(
|
|||||||
from_warehouses,
|
from_warehouses,
|
||||||
required_qty,
|
required_qty,
|
||||||
company,
|
company,
|
||||||
total_picked_qty=0,
|
|
||||||
consider_rejected_warehouses=False,
|
consider_rejected_warehouses=False,
|
||||||
):
|
):
|
||||||
# Get batch nos by FIFO
|
# Get batch nos by FIFO
|
||||||
locations = get_available_item_locations_for_batched_item(
|
locations = get_available_item_locations_for_batched_item(
|
||||||
item_code,
|
item_code,
|
||||||
from_warehouses,
|
from_warehouses,
|
||||||
required_qty,
|
|
||||||
company,
|
|
||||||
consider_rejected_warehouses=consider_rejected_warehouses,
|
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -821,7 +878,6 @@ def get_available_item_locations_for_serial_and_batched_item(
|
|||||||
(conditions) & (sn.batch_no == location.batch_no) & (sn.warehouse == location.warehouse)
|
(conditions) & (sn.batch_no == location.batch_no) & (sn.warehouse == location.warehouse)
|
||||||
)
|
)
|
||||||
.orderby(sn.creation)
|
.orderby(sn.creation)
|
||||||
.limit(ceil(location.qty + total_picked_qty))
|
|
||||||
).run(as_dict=True)
|
).run(as_dict=True)
|
||||||
|
|
||||||
serial_nos = [sn.name for sn in serial_nos]
|
serial_nos = [sn.name for sn in serial_nos]
|
||||||
@@ -834,18 +890,14 @@ def get_available_item_locations_for_serial_and_batched_item(
|
|||||||
def get_available_item_locations_for_serialized_item(
|
def get_available_item_locations_for_serialized_item(
|
||||||
item_code,
|
item_code,
|
||||||
from_warehouses,
|
from_warehouses,
|
||||||
required_qty,
|
|
||||||
company,
|
company,
|
||||||
total_picked_qty=0,
|
|
||||||
consider_rejected_warehouses=False,
|
consider_rejected_warehouses=False,
|
||||||
):
|
):
|
||||||
picked_serial_nos = get_picked_serial_nos(item_code, from_warehouses)
|
|
||||||
|
|
||||||
sn = frappe.qb.DocType("Serial No")
|
sn = frappe.qb.DocType("Serial No")
|
||||||
query = (
|
query = (
|
||||||
frappe.qb.from_(sn)
|
frappe.qb.from_(sn)
|
||||||
.select(sn.name, sn.warehouse)
|
.select(sn.name, sn.warehouse)
|
||||||
.where((sn.item_code == item_code) & (sn.company == company))
|
.where(sn.item_code == item_code)
|
||||||
.orderby(sn.creation)
|
.orderby(sn.creation)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -853,6 +905,7 @@ def get_available_item_locations_for_serialized_item(
|
|||||||
query = query.where(sn.warehouse.isin(from_warehouses))
|
query = query.where(sn.warehouse.isin(from_warehouses))
|
||||||
else:
|
else:
|
||||||
query = query.where(Coalesce(sn.warehouse, "") != "")
|
query = query.where(Coalesce(sn.warehouse, "") != "")
|
||||||
|
query = query.where(sn.company == company)
|
||||||
|
|
||||||
if not consider_rejected_warehouses:
|
if not consider_rejected_warehouses:
|
||||||
if rejected_warehouses := get_rejected_warehouses():
|
if rejected_warehouses := get_rejected_warehouses():
|
||||||
@@ -861,16 +914,8 @@ def get_available_item_locations_for_serialized_item(
|
|||||||
serial_nos = query.run(as_list=True)
|
serial_nos = query.run(as_list=True)
|
||||||
|
|
||||||
warehouse_serial_nos_map = frappe._dict()
|
warehouse_serial_nos_map = frappe._dict()
|
||||||
picked_qty = required_qty
|
|
||||||
for serial_no, warehouse in serial_nos:
|
for serial_no, warehouse in serial_nos:
|
||||||
if serial_no in picked_serial_nos:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if picked_qty <= 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
warehouse_serial_nos_map.setdefault(warehouse, []).append(serial_no)
|
warehouse_serial_nos_map.setdefault(warehouse, []).append(serial_no)
|
||||||
picked_qty -= 1
|
|
||||||
|
|
||||||
locations = []
|
locations = []
|
||||||
|
|
||||||
@@ -878,12 +923,14 @@ def get_available_item_locations_for_serialized_item(
|
|||||||
qty = len(serial_nos)
|
qty = len(serial_nos)
|
||||||
|
|
||||||
locations.append(
|
locations.append(
|
||||||
{
|
frappe._dict(
|
||||||
"qty": qty,
|
{
|
||||||
"warehouse": warehouse,
|
"qty": qty,
|
||||||
"item_code": item_code,
|
"warehouse": warehouse,
|
||||||
"serial_nos": serial_nos,
|
"item_code": item_code,
|
||||||
}
|
"serial_nos": serial_nos,
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return locations
|
return locations
|
||||||
@@ -892,9 +939,6 @@ def get_available_item_locations_for_serialized_item(
|
|||||||
def get_available_item_locations_for_batched_item(
|
def get_available_item_locations_for_batched_item(
|
||||||
item_code,
|
item_code,
|
||||||
from_warehouses,
|
from_warehouses,
|
||||||
required_qty,
|
|
||||||
company,
|
|
||||||
total_picked_qty=0,
|
|
||||||
consider_rejected_warehouses=False,
|
consider_rejected_warehouses=False,
|
||||||
):
|
):
|
||||||
locations = []
|
locations = []
|
||||||
@@ -903,8 +947,6 @@ def get_available_item_locations_for_batched_item(
|
|||||||
{
|
{
|
||||||
"item_code": item_code,
|
"item_code": item_code,
|
||||||
"warehouse": from_warehouses,
|
"warehouse": from_warehouses,
|
||||||
"qty": required_qty,
|
|
||||||
"is_pick_list": True,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -940,9 +982,7 @@ def get_available_item_locations_for_batched_item(
|
|||||||
def get_available_item_locations_for_other_item(
|
def get_available_item_locations_for_other_item(
|
||||||
item_code,
|
item_code,
|
||||||
from_warehouses,
|
from_warehouses,
|
||||||
required_qty,
|
|
||||||
company,
|
company,
|
||||||
total_picked_qty=0,
|
|
||||||
consider_rejected_warehouses=False,
|
consider_rejected_warehouses=False,
|
||||||
):
|
):
|
||||||
bin = frappe.qb.DocType("Bin")
|
bin = frappe.qb.DocType("Bin")
|
||||||
@@ -951,7 +991,6 @@ def get_available_item_locations_for_other_item(
|
|||||||
.select(bin.warehouse, bin.actual_qty.as_("qty"))
|
.select(bin.warehouse, bin.actual_qty.as_("qty"))
|
||||||
.where((bin.item_code == item_code) & (bin.actual_qty > 0))
|
.where((bin.item_code == item_code) & (bin.actual_qty > 0))
|
||||||
.orderby(bin.creation)
|
.orderby(bin.creation)
|
||||||
.limit(cint(required_qty + total_picked_qty))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if from_warehouses:
|
if from_warehouses:
|
||||||
|
|||||||
@@ -815,7 +815,7 @@ class TestPickList(FrappeTestCase):
|
|||||||
|
|
||||||
def test_pick_list_status(self):
|
def test_pick_list_status(self):
|
||||||
warehouse = "_Test Warehouse - _TC"
|
warehouse = "_Test Warehouse - _TC"
|
||||||
item = make_item(properties={"maintain_stock": 1}).name
|
item = make_item(properties={"is_stock_item": 1}).name
|
||||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=10)
|
make_stock_entry(item=item, to_warehouse=warehouse, qty=10)
|
||||||
|
|
||||||
so = make_sales_order(item_code=item, qty=10, rate=100)
|
so = make_sales_order(item_code=item, qty=10, rate=100)
|
||||||
@@ -845,3 +845,135 @@ class TestPickList(FrappeTestCase):
|
|||||||
pl.cancel()
|
pl.cancel()
|
||||||
pl.reload()
|
pl.reload()
|
||||||
self.assertEqual(pl.status, "Cancelled")
|
self.assertEqual(pl.status, "Cancelled")
|
||||||
|
|
||||||
|
def test_pick_list_validation(self):
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
item = make_item("Test Non Serialized Pick List Item", properties={"is_stock_item": 1}).name
|
||||||
|
|
||||||
|
make_stock_entry(item=item, to_warehouse=warehouse, qty=10)
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=5, rate=100)
|
||||||
|
|
||||||
|
pl = create_pick_list(so.name)
|
||||||
|
pl.save()
|
||||||
|
pl.submit()
|
||||||
|
self.assertEqual(pl.locations[0].qty, 5.0)
|
||||||
|
self.assertTrue(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=5, rate=100)
|
||||||
|
|
||||||
|
pl = create_pick_list(so.name)
|
||||||
|
pl.save()
|
||||||
|
self.assertEqual(pl.locations[0].qty, 5.0)
|
||||||
|
self.assertTrue(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=4, rate=100)
|
||||||
|
pl = create_pick_list(so.name)
|
||||||
|
self.assertFalse(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
def test_pick_list_validation_for_serial_no(self):
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
item = make_item(
|
||||||
|
"Test Serialized Pick List Item",
|
||||||
|
properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SN-SPLI-.####"},
|
||||||
|
).name
|
||||||
|
|
||||||
|
make_stock_entry(item=item, to_warehouse=warehouse, qty=10)
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=5, rate=100)
|
||||||
|
|
||||||
|
pl = create_pick_list(so.name)
|
||||||
|
pl.locations[0].qty = 5
|
||||||
|
pl.save()
|
||||||
|
pl.submit()
|
||||||
|
self.assertTrue(pl.locations[0].serial_no)
|
||||||
|
self.assertEqual(pl.locations[0].qty, 5.0)
|
||||||
|
self.assertTrue(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=5, rate=100)
|
||||||
|
|
||||||
|
pl = create_pick_list(so.name)
|
||||||
|
pl.save()
|
||||||
|
self.assertTrue(pl.locations[0].serial_no)
|
||||||
|
self.assertEqual(pl.locations[0].qty, 5.0)
|
||||||
|
self.assertTrue(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=4, rate=100)
|
||||||
|
pl = create_pick_list(so.name)
|
||||||
|
self.assertFalse(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
def test_pick_list_validation_for_batch_no(self):
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
item = make_item(
|
||||||
|
"Test Batch Pick List Item",
|
||||||
|
properties={
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"batch_number_series": "BATCH-SPLI-.####",
|
||||||
|
"create_new_batch": 1,
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
make_stock_entry(item=item, to_warehouse=warehouse, qty=10)
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=5, rate=100)
|
||||||
|
|
||||||
|
pl = create_pick_list(so.name)
|
||||||
|
pl.locations[0].qty = 5
|
||||||
|
pl.save()
|
||||||
|
pl.submit()
|
||||||
|
self.assertTrue(pl.locations[0].batch_no)
|
||||||
|
self.assertEqual(pl.locations[0].qty, 5.0)
|
||||||
|
self.assertTrue(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=5, rate=100)
|
||||||
|
|
||||||
|
pl = create_pick_list(so.name)
|
||||||
|
pl.save()
|
||||||
|
self.assertTrue(pl.locations[0].batch_no)
|
||||||
|
self.assertEqual(pl.locations[0].qty, 5.0)
|
||||||
|
self.assertTrue(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=4, rate=100)
|
||||||
|
pl = create_pick_list(so.name)
|
||||||
|
self.assertFalse(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
def test_pick_list_validation_for_batch_no_and_serial_item(self):
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
item = make_item(
|
||||||
|
"Test Serialized Batch Pick List Item",
|
||||||
|
properties={
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"batch_number_series": "SN-BT-BATCH-SPLI-.####",
|
||||||
|
"create_new_batch": 1,
|
||||||
|
"has_serial_no": 1,
|
||||||
|
"serial_no_series": "SN-BT-SPLI-.####",
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
make_stock_entry(item=item, to_warehouse=warehouse, qty=10)
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=5, rate=100)
|
||||||
|
|
||||||
|
pl = create_pick_list(so.name)
|
||||||
|
pl.locations[0].qty = 5
|
||||||
|
pl.save()
|
||||||
|
pl.submit()
|
||||||
|
self.assertTrue(pl.locations[0].batch_no)
|
||||||
|
self.assertTrue(pl.locations[0].serial_no)
|
||||||
|
self.assertEqual(pl.locations[0].qty, 5.0)
|
||||||
|
self.assertTrue(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=5, rate=100)
|
||||||
|
|
||||||
|
pl = create_pick_list(so.name)
|
||||||
|
pl.save()
|
||||||
|
self.assertTrue(pl.locations[0].batch_no)
|
||||||
|
self.assertTrue(pl.locations[0].serial_no)
|
||||||
|
self.assertEqual(pl.locations[0].qty, 5.0)
|
||||||
|
self.assertTrue(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=4, rate=100)
|
||||||
|
pl = create_pick_list(so.name)
|
||||||
|
self.assertFalse(hasattr(pl, "locations"))
|
||||||
|
|||||||
@@ -58,6 +58,8 @@
|
|||||||
"column_break_27",
|
"column_break_27",
|
||||||
"total",
|
"total",
|
||||||
"net_total",
|
"net_total",
|
||||||
|
"tax_withholding_net_total",
|
||||||
|
"base_tax_withholding_net_total",
|
||||||
"taxes_charges_section",
|
"taxes_charges_section",
|
||||||
"tax_category",
|
"tax_category",
|
||||||
"taxes_and_charges",
|
"taxes_and_charges",
|
||||||
@@ -1246,13 +1248,31 @@
|
|||||||
"label": "Subcontracting Receipt",
|
"label": "Subcontracting Receipt",
|
||||||
"options": "Subcontracting Receipt",
|
"options": "Subcontracting Receipt",
|
||||||
"search_index": 1
|
"search_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "tax_withholding_net_total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Tax Withholding Net Total",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "currency",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "base_tax_withholding_net_total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Base Tax Withholding Net Total",
|
||||||
|
"no_copy": 1,
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-truck",
|
"icon": "fa fa-truck",
|
||||||
"idx": 261,
|
"idx": 261,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:25.441066",
|
"modified": "2024-04-08 20:23:03.699201",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Purchase Receipt",
|
"name": "Purchase Receipt",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from erpnext.accounts.utils import get_account_currency
|
|||||||
from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled
|
from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled
|
||||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||||
from erpnext.buying.utils import check_on_hold_or_closed_status
|
from erpnext.buying.utils import check_on_hold_or_closed_status
|
||||||
|
from erpnext.controllers.accounts_controller import merge_taxes
|
||||||
from erpnext.controllers.buying_controller import BuyingController
|
from erpnext.controllers.buying_controller import BuyingController
|
||||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction
|
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction
|
||||||
|
|
||||||
@@ -51,6 +52,7 @@ class PurchaseReceipt(BuyingController):
|
|||||||
base_net_total: DF.Currency
|
base_net_total: DF.Currency
|
||||||
base_rounded_total: DF.Currency
|
base_rounded_total: DF.Currency
|
||||||
base_rounding_adjustment: DF.Currency
|
base_rounding_adjustment: DF.Currency
|
||||||
|
base_tax_withholding_net_total: DF.Currency
|
||||||
base_taxes_and_charges_added: DF.Currency
|
base_taxes_and_charges_added: DF.Currency
|
||||||
base_taxes_and_charges_deducted: DF.Currency
|
base_taxes_and_charges_deducted: DF.Currency
|
||||||
base_total: DF.Currency
|
base_total: DF.Currency
|
||||||
@@ -120,6 +122,7 @@ class PurchaseReceipt(BuyingController):
|
|||||||
supplier_name: DF.Data | None
|
supplier_name: DF.Data | None
|
||||||
supplier_warehouse: DF.Link | None
|
supplier_warehouse: DF.Link | None
|
||||||
tax_category: DF.Link | None
|
tax_category: DF.Link | None
|
||||||
|
tax_withholding_net_total: DF.Currency
|
||||||
taxes: DF.Table[PurchaseTaxesandCharges]
|
taxes: DF.Table[PurchaseTaxesandCharges]
|
||||||
taxes_and_charges: DF.Link | None
|
taxes_and_charges: DF.Link | None
|
||||||
taxes_and_charges_added: DF.Currency
|
taxes_and_charges_added: DF.Currency
|
||||||
@@ -1123,37 +1126,6 @@ def get_item_wise_returned_qty(pr_doc):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def merge_taxes(source_taxes, target_doc):
|
|
||||||
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
|
|
||||||
update_item_wise_tax_detail,
|
|
||||||
)
|
|
||||||
|
|
||||||
existing_taxes = target_doc.get("taxes") or []
|
|
||||||
idx = 1
|
|
||||||
for tax in source_taxes:
|
|
||||||
found = False
|
|
||||||
for t in existing_taxes:
|
|
||||||
if t.account_head == tax.account_head and t.cost_center == tax.cost_center:
|
|
||||||
t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount)
|
|
||||||
t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount)
|
|
||||||
update_item_wise_tax_detail(t, tax)
|
|
||||||
found = True
|
|
||||||
|
|
||||||
if not found:
|
|
||||||
tax.charge_type = "Actual"
|
|
||||||
tax.idx = idx
|
|
||||||
idx += 1
|
|
||||||
tax.included_in_print_rate = 0
|
|
||||||
tax.dont_recompute_tax = 1
|
|
||||||
tax.row_id = ""
|
|
||||||
tax.tax_amount = tax.tax_amount_after_discount_amount
|
|
||||||
tax.base_tax_amount = tax.base_tax_amount_after_discount_amount
|
|
||||||
tax.item_wise_tax_detail = tax.item_wise_tax_detail
|
|
||||||
existing_taxes.append(tax)
|
|
||||||
|
|
||||||
target_doc.set("taxes", existing_taxes)
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_purchase_invoice(source_name, target_doc=None, args=None):
|
def make_purchase_invoice(source_name, target_doc=None, args=None):
|
||||||
from erpnext.accounts.party import get_payment_terms_template
|
from erpnext.accounts.party import get_payment_terms_template
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
"pricing_rules",
|
"pricing_rules",
|
||||||
"stock_uom_rate",
|
"stock_uom_rate",
|
||||||
"is_free_item",
|
"is_free_item",
|
||||||
|
"apply_tds",
|
||||||
"section_break_29",
|
"section_break_29",
|
||||||
"net_rate",
|
"net_rate",
|
||||||
"net_amount",
|
"net_amount",
|
||||||
@@ -1107,12 +1108,20 @@
|
|||||||
"fieldname": "use_serial_batch_fields",
|
"fieldname": "use_serial_batch_fields",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Use Serial No / Batch Fields"
|
"label": "Use Serial No / Batch Fields"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "apply_tds",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Apply TDS",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:25.896543",
|
"modified": "2024-04-08 20:00:16.277292",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Purchase Receipt Item",
|
"name": "Purchase Receipt Item",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class PurchaseReceiptItem(Document):
|
|||||||
|
|
||||||
allow_zero_valuation_rate: DF.Check
|
allow_zero_valuation_rate: DF.Check
|
||||||
amount: DF.Currency
|
amount: DF.Currency
|
||||||
|
apply_tds: DF.Check
|
||||||
asset_category: DF.Link | None
|
asset_category: DF.Link | None
|
||||||
asset_location: DF.Link | None
|
asset_location: DF.Link | None
|
||||||
barcode: DF.Data | None
|
barcode: DF.Data | None
|
||||||
|
|||||||
@@ -860,6 +860,12 @@ class SerialandBatchBundle(Document):
|
|||||||
self.validate_batch_inventory()
|
self.validate_batch_inventory()
|
||||||
|
|
||||||
def validate_batch_inventory(self):
|
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:
|
if not self.has_batch_no:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,17 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.query_builder.functions import Sum
|
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
|
import erpnext
|
||||||
from erpnext.accounts.general_ledger import process_gl_map
|
from erpnext.accounts.general_ledger import process_gl_map
|
||||||
@@ -30,6 +40,7 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
|
|||||||
OpeningEntryAccountError,
|
OpeningEntryAccountError,
|
||||||
)
|
)
|
||||||
from erpnext.stock.get_item_details import (
|
from erpnext.stock.get_item_details import (
|
||||||
|
get_barcode_data,
|
||||||
get_bin_details,
|
get_bin_details,
|
||||||
get_conversion_factor,
|
get_conversion_factor,
|
||||||
get_default_cost_center,
|
get_default_cost_center,
|
||||||
@@ -428,7 +439,14 @@ class StockEntry(StockController):
|
|||||||
for field in reset_fields:
|
for field in reset_fields:
|
||||||
item.set(field, item_details.get(field))
|
item.set(field, item_details.get(field))
|
||||||
|
|
||||||
update_fields = ("uom", "description", "expense_account", "cost_center", "conversion_factor")
|
update_fields = (
|
||||||
|
"uom",
|
||||||
|
"description",
|
||||||
|
"expense_account",
|
||||||
|
"cost_center",
|
||||||
|
"conversion_factor",
|
||||||
|
"barcode",
|
||||||
|
)
|
||||||
|
|
||||||
for field in update_fields:
|
for field in update_fields:
|
||||||
if not item.get(field):
|
if not item.get(field):
|
||||||
@@ -637,8 +655,8 @@ class StockEntry(StockController):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
work_order_link = frappe.utils.get_link_to_form("Work Order", self.work_order)
|
work_order_link = get_link_to_form("Work Order", self.work_order)
|
||||||
job_card_link = frappe.utils.get_link_to_form("Job Card", job_card)
|
job_card_link = get_link_to_form("Job Card", job_card)
|
||||||
frappe.throw(
|
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}."
|
"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}."
|
||||||
@@ -1347,9 +1365,24 @@ class StockEntry(StockController):
|
|||||||
|
|
||||||
return finished_item_row
|
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):
|
def get_sle_for_source_warehouse(self, sl_entries, finished_item_row):
|
||||||
for d in self.get("items"):
|
for d in self.get("items"):
|
||||||
if cstr(d.s_warehouse):
|
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(
|
sle = self.get_sl_entries(
|
||||||
d,
|
d,
|
||||||
{
|
{
|
||||||
@@ -1366,6 +1399,21 @@ class StockEntry(StockController):
|
|||||||
):
|
):
|
||||||
sle.dependant_sle_voucher_detail_no = finished_item_row.name
|
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)
|
sl_entries.append(sle)
|
||||||
|
|
||||||
def make_serial_and_batch_bundle_for_transfer(self):
|
def make_serial_and_batch_bundle_for_transfer(self):
|
||||||
@@ -1634,6 +1682,10 @@ class StockEntry(StockController):
|
|||||||
if subcontract_items and len(subcontract_items) == 1:
|
if subcontract_items and len(subcontract_items) == 1:
|
||||||
ret["subcontracted_item"] = subcontract_items[0].main_item_code
|
ret["subcontracted_item"] = subcontract_items[0].main_item_code
|
||||||
|
|
||||||
|
barcode_data = get_barcode_data(item_code=item.name)
|
||||||
|
if barcode_data and len(barcode_data.get(item.name)) == 1:
|
||||||
|
ret["barcode"] = barcode_data.get(item.name)[0]
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
@@ -110,6 +110,12 @@ class TestStockEntry(FrappeTestCase):
|
|||||||
self._test_auto_material_request("_Test Item")
|
self._test_auto_material_request("_Test Item")
|
||||||
self._test_auto_material_request("_Test Item", material_request_type="Transfer")
|
self._test_auto_material_request("_Test Item", material_request_type="Transfer")
|
||||||
|
|
||||||
|
def test_barcode_item_stock_entry(self):
|
||||||
|
item_code = make_item("_Test Item Stock Entry For Barcode", barcode="BDD-1234567890")
|
||||||
|
|
||||||
|
se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100)
|
||||||
|
self.assertEqual(se.items[0].barcode, "BDD-1234567890")
|
||||||
|
|
||||||
def test_auto_material_request_for_variant(self):
|
def test_auto_material_request_for_variant(self):
|
||||||
fields = [{"field_name": "reorder_levels"}]
|
fields = [{"field_name": "reorder_levels"}]
|
||||||
set_item_variant_settings(fields)
|
set_item_variant_settings(fields)
|
||||||
@@ -1748,6 +1754,41 @@ class TestStockEntry(FrappeTestCase):
|
|||||||
self.assertTrue(frappe.db.exists("Serial No", serial_no))
|
self.assertTrue(frappe.db.exists("Serial No", serial_no))
|
||||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Delivered")
|
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):
|
def make_serialized_item(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|||||||
@@ -81,6 +81,18 @@ frappe.ui.form.on("Stock Reconciliation", {
|
|||||||
if (frm.doc.company) {
|
if (frm.doc.company) {
|
||||||
frm.trigger("toggle_display_account_head");
|
frm.trigger("toggle_display_account_head");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frm.events.set_fields_onload_for_line_item(frm);
|
||||||
|
},
|
||||||
|
|
||||||
|
set_fields_onload_for_line_item(frm) {
|
||||||
|
if (frm.is_new() && frm.doc?.items && cint(frappe.user_defaults?.use_serial_batch_fields) === 1) {
|
||||||
|
frm.doc.items.forEach((item) => {
|
||||||
|
if (!item.serial_and_batch_bundle) {
|
||||||
|
frappe.model.set_value(item.doctype, item.name, "use_serial_batch_fields", 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
scan_barcode: function (frm) {
|
scan_barcode: function (frm) {
|
||||||
@@ -155,6 +167,9 @@ frappe.ui.form.on("Stock Reconciliation", {
|
|||||||
|
|
||||||
item.qty = item.qty || 0;
|
item.qty = item.qty || 0;
|
||||||
item.valuation_rate = item.valuation_rate || 0;
|
item.valuation_rate = item.valuation_rate || 0;
|
||||||
|
item.use_serial_batch_fields = cint(
|
||||||
|
frappe.user_defaults?.use_serial_batch_fields
|
||||||
|
);
|
||||||
});
|
});
|
||||||
frm.refresh_field("items");
|
frm.refresh_field("items");
|
||||||
},
|
},
|
||||||
@@ -298,6 +313,10 @@ frappe.ui.form.on("Stock Reconciliation Item", {
|
|||||||
if (!item.warehouse && frm.doc.set_warehouse) {
|
if (!item.warehouse && frm.doc.set_warehouse) {
|
||||||
frappe.model.set_value(cdt, cdn, "warehouse", frm.doc.set_warehouse);
|
frappe.model.set_value(cdt, cdn, "warehouse", frm.doc.set_warehouse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.docstatus === 0 && cint(frappe.user_defaults?.use_serial_batch_fields) === 1) {
|
||||||
|
frappe.model.set_value(item.doctype, item.name, "use_serial_batch_fields", 1);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
add_serial_batch_bundle(frm, cdt, cdn) {
|
add_serial_batch_bundle(frm, cdt, cdn) {
|
||||||
|
|||||||
@@ -1021,7 +1021,9 @@ def get_batch_qty_for_stock_reco(item_code, warehouse, batch_no, posting_date, p
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_items(warehouse, posting_date, posting_time, company, item_code=None, ignore_empty_stock=False):
|
def get_items(warehouse, posting_date, posting_time, company, item_code=None, ignore_empty_stock=False):
|
||||||
ignore_empty_stock = cint(ignore_empty_stock)
|
ignore_empty_stock = cint(ignore_empty_stock)
|
||||||
items = [frappe._dict({"item_code": item_code, "warehouse": warehouse})]
|
items = []
|
||||||
|
if item_code and warehouse:
|
||||||
|
items = get_item_and_warehouses(item_code, warehouse)
|
||||||
|
|
||||||
if not item_code:
|
if not item_code:
|
||||||
items = get_items_for_stock_reco(warehouse, company)
|
items = get_items_for_stock_reco(warehouse, company)
|
||||||
@@ -1066,6 +1068,20 @@ def get_items(warehouse, posting_date, posting_time, company, item_code=None, ig
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def get_item_and_warehouses(item_code, warehouse):
|
||||||
|
from frappe.utils.nestedset import get_descendants_of
|
||||||
|
|
||||||
|
items = []
|
||||||
|
if frappe.get_cached_value("Warehouse", warehouse, "is_group"):
|
||||||
|
childrens = get_descendants_of("Warehouse", warehouse, ignore_permissions=True, order_by="lft")
|
||||||
|
for ch_warehouse in childrens:
|
||||||
|
items.append(frappe._dict({"item_code": item_code, "warehouse": ch_warehouse}))
|
||||||
|
else:
|
||||||
|
items = [frappe._dict({"item_code": item_code, "warehouse": warehouse})]
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
def get_items_for_stock_reco(warehouse, company):
|
def get_items_for_stock_reco(warehouse, company):
|
||||||
lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"])
|
lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"])
|
||||||
items = frappe.db.sql(
|
items = frappe.db.sql(
|
||||||
@@ -1080,7 +1096,7 @@ def get_items_for_stock_reco(warehouse, company):
|
|||||||
and i.is_stock_item = 1
|
and i.is_stock_item = 1
|
||||||
and i.has_variants = 0
|
and i.has_variants = 0
|
||||||
and exists(
|
and exists(
|
||||||
select name from `tabWarehouse` where lft >= {lft} and rgt <= {rgt} and name = bin.warehouse
|
select name from `tabWarehouse` where lft >= {lft} and rgt <= {rgt} and name = bin.warehouse and is_group = 0
|
||||||
)
|
)
|
||||||
""",
|
""",
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
@@ -1095,7 +1111,7 @@ def get_items_for_stock_reco(warehouse, company):
|
|||||||
where
|
where
|
||||||
i.name = id.parent
|
i.name = id.parent
|
||||||
and exists(
|
and exists(
|
||||||
select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse
|
select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse and is_group = 0
|
||||||
)
|
)
|
||||||
and i.is_stock_item = 1
|
and i.is_stock_item = 1
|
||||||
and i.has_variants = 0
|
and i.has_variants = 0
|
||||||
@@ -1157,7 +1173,7 @@ def get_itemwise_batch(warehouse, posting_date, company, item_code=None):
|
|||||||
frappe._dict(
|
frappe._dict(
|
||||||
{
|
{
|
||||||
"item_code": row[0],
|
"item_code": row[0],
|
||||||
"warehouse": warehouse,
|
"warehouse": row[3],
|
||||||
"qty": row[8],
|
"qty": row[8],
|
||||||
"item_name": row[1],
|
"item_name": row[1],
|
||||||
"batch_no": row[4],
|
"batch_no": row[4],
|
||||||
|
|||||||
@@ -308,3 +308,13 @@ def clean_all_descriptions():
|
|||||||
clean_description = clean_html(item.description)
|
clean_description = clean_html(item.description)
|
||||||
if item.description != clean_description:
|
if item.description != clean_description:
|
||||||
frappe.db.set_value("Item", item.name, "description", clean_description)
|
frappe.db.set_value("Item", item.name, "description", clean_description)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_enable_stock_uom_editing():
|
||||||
|
return frappe.get_cached_value(
|
||||||
|
"Stock Settings",
|
||||||
|
None,
|
||||||
|
["allow_to_edit_stock_uom_qty_for_sales", "allow_to_edit_stock_uom_qty_for_purchase"],
|
||||||
|
as_dict=1,
|
||||||
|
)
|
||||||
|
|||||||
@@ -490,12 +490,21 @@ def update_barcode_value(out):
|
|||||||
out["barcode"] = barcode_data.get(out.item_code)[0]
|
out["barcode"] = barcode_data.get(out.item_code)[0]
|
||||||
|
|
||||||
|
|
||||||
def get_barcode_data(items_list):
|
def get_barcode_data(items_list=None, item_code=None):
|
||||||
# get item-wise batch no data
|
# get item-wise batch no data
|
||||||
# example: {'LED-GRE': [Batch001, Batch002]}
|
# example: {'LED-GRE': [Batch001, Batch002]}
|
||||||
# where LED-GRE is item code, SN0001 is serial no and Pune is warehouse
|
# where LED-GRE is item code, SN0001 is serial no and Pune is warehouse
|
||||||
|
|
||||||
itemwise_barcode = {}
|
itemwise_barcode = {}
|
||||||
|
if not items_list and item_code:
|
||||||
|
_dict_item_code = frappe._dict(
|
||||||
|
{
|
||||||
|
"item_code": item_code,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
items_list = [frappe._dict(_dict_item_code)]
|
||||||
|
|
||||||
for item in items_list:
|
for item in items_list:
|
||||||
barcodes = frappe.db.get_all("Item Barcode", filters={"parent": item.item_code}, fields="barcode")
|
barcodes = frappe.db.get_all("Item Barcode", filters={"parent": item.item_code}, fields="barcode")
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ frappe.query_reports["Item Prices"] = {
|
|||||||
fieldtype: "Select",
|
fieldtype: "Select",
|
||||||
options: "Enabled Items only\nDisabled Items only\nAll Items",
|
options: "Enabled Items only\nDisabled Items only\nAll Items",
|
||||||
default: "Enabled Items only",
|
default: "Enabled Items only",
|
||||||
on_change: function (query_report) {
|
|
||||||
query_report.trigger_refresh();
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -498,8 +498,6 @@ class SubcontractingReceipt(SubcontractingController):
|
|||||||
return process_gl_map(gl_entries)
|
return process_gl_map(gl_entries)
|
||||||
|
|
||||||
def make_item_gl_entries(self, gl_entries, warehouse_account=None):
|
def make_item_gl_entries(self, gl_entries, warehouse_account=None):
|
||||||
stock_rbnb = self.get_company_default("stock_received_but_not_billed")
|
|
||||||
|
|
||||||
warehouse_with_no_account = []
|
warehouse_with_no_account = []
|
||||||
|
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
@@ -517,31 +515,41 @@ class SubcontractingReceipt(SubcontractingController):
|
|||||||
"stock_value_difference",
|
"stock_value_difference",
|
||||||
)
|
)
|
||||||
|
|
||||||
warehouse_account_name = warehouse_account[item.warehouse]["account"]
|
accepted_warehouse_account = warehouse_account[item.warehouse]["account"]
|
||||||
warehouse_account_currency = warehouse_account[item.warehouse]["account_currency"]
|
|
||||||
supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get(
|
supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get(
|
||||||
"account"
|
"account"
|
||||||
)
|
)
|
||||||
supplier_warehouse_account_currency = warehouse_account.get(
|
|
||||||
self.supplier_warehouse, {}
|
|
||||||
).get("account_currency")
|
|
||||||
remarks = self.get("remarks") or _("Accounting Entry for Stock")
|
remarks = self.get("remarks") or _("Accounting Entry for Stock")
|
||||||
|
|
||||||
# FG Warehouse Account (Debit)
|
# Accepted Warehouse Account (Debit)
|
||||||
self.add_gl_entry(
|
self.add_gl_entry(
|
||||||
gl_entries=gl_entries,
|
gl_entries=gl_entries,
|
||||||
account=warehouse_account_name,
|
account=accepted_warehouse_account,
|
||||||
cost_center=item.cost_center,
|
cost_center=item.cost_center,
|
||||||
debit=stock_value_diff,
|
debit=stock_value_diff,
|
||||||
credit=0.0,
|
credit=0.0,
|
||||||
remarks=remarks,
|
remarks=remarks,
|
||||||
against_account=stock_rbnb,
|
against_account=item.expense_account,
|
||||||
account_currency=warehouse_account_currency,
|
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,
|
item=item,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Supplier Warehouse Account (Credit)
|
if flt(item.rm_supp_cost) and supplier_warehouse_account:
|
||||||
if flt(item.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse):
|
# Supplier Warehouse Account (Credit)
|
||||||
self.add_gl_entry(
|
self.add_gl_entry(
|
||||||
gl_entries=gl_entries,
|
gl_entries=gl_entries,
|
||||||
account=supplier_warehouse_account,
|
account=supplier_warehouse_account,
|
||||||
@@ -549,40 +557,66 @@ class SubcontractingReceipt(SubcontractingController):
|
|||||||
debit=0.0,
|
debit=0.0,
|
||||||
credit=flt(item.rm_supp_cost),
|
credit=flt(item.rm_supp_cost),
|
||||||
remarks=remarks,
|
remarks=remarks,
|
||||||
against_account=warehouse_account_name,
|
against_account=item.expense_account,
|
||||||
account_currency=supplier_warehouse_account_currency,
|
account_currency=get_account_currency(supplier_warehouse_account),
|
||||||
|
project=item.project,
|
||||||
item=item,
|
item=item,
|
||||||
)
|
)
|
||||||
|
# Expense Account (Debit)
|
||||||
# Expense Account (Credit)
|
|
||||||
if flt(item.service_cost_per_qty):
|
|
||||||
self.add_gl_entry(
|
self.add_gl_entry(
|
||||||
gl_entries=gl_entries,
|
gl_entries=gl_entries,
|
||||||
account=item.expense_account,
|
account=item.expense_account,
|
||||||
cost_center=item.cost_center,
|
cost_center=item.cost_center,
|
||||||
debit=0.0,
|
debit=flt(item.rm_supp_cost),
|
||||||
credit=flt(item.service_cost_per_qty) * flt(item.qty),
|
credit=0.0,
|
||||||
remarks=remarks,
|
remarks=remarks,
|
||||||
against_account=warehouse_account_name,
|
against_account=supplier_warehouse_account,
|
||||||
account_currency=get_account_currency(item.expense_account),
|
account_currency=get_account_currency(item.expense_account),
|
||||||
|
project=item.project,
|
||||||
item=item,
|
item=item,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Loss Account (Credit)
|
# Expense Account (Debit)
|
||||||
divisional_loss = flt(item.amount - stock_value_diff, item.precision("amount"))
|
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 divisional_loss := flt(item.amount - stock_value_diff, item.precision("amount")):
|
||||||
loss_account = item.expense_account
|
loss_account = self.get_company_default(
|
||||||
|
"stock_adjustment_account", ignore_validation=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Loss Account (Credit)
|
||||||
self.add_gl_entry(
|
self.add_gl_entry(
|
||||||
gl_entries=gl_entries,
|
gl_entries=gl_entries,
|
||||||
account=loss_account,
|
account=loss_account,
|
||||||
cost_center=item.cost_center,
|
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,
|
debit=divisional_loss,
|
||||||
credit=0.0,
|
credit=0.0,
|
||||||
remarks=remarks,
|
remarks=remarks,
|
||||||
against_account=warehouse_account_name,
|
against_account=loss_account,
|
||||||
account_currency=get_account_currency(loss_account),
|
account_currency=get_account_currency(item.expense_account),
|
||||||
project=item.project,
|
project=item.project,
|
||||||
item=item,
|
item=item,
|
||||||
)
|
)
|
||||||
@@ -592,7 +626,6 @@ class SubcontractingReceipt(SubcontractingController):
|
|||||||
):
|
):
|
||||||
warehouse_with_no_account.append(item.warehouse)
|
warehouse_with_no_account.append(item.warehouse)
|
||||||
|
|
||||||
# Additional Costs Expense Accounts (Credit)
|
|
||||||
for row in self.additional_costs:
|
for row in self.additional_costs:
|
||||||
credit_amount = (
|
credit_amount = (
|
||||||
flt(row.base_amount)
|
flt(row.base_amount)
|
||||||
@@ -600,6 +633,7 @@ class SubcontractingReceipt(SubcontractingController):
|
|||||||
else flt(row.amount)
|
else flt(row.amount)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Additional Cost Expense Account (Credit)
|
||||||
self.add_gl_entry(
|
self.add_gl_entry(
|
||||||
gl_entries=gl_entries,
|
gl_entries=gl_entries,
|
||||||
account=row.expense_account,
|
account=row.expense_account,
|
||||||
@@ -608,6 +642,7 @@ class SubcontractingReceipt(SubcontractingController):
|
|||||||
credit=credit_amount,
|
credit=credit_amount,
|
||||||
remarks=remarks,
|
remarks=remarks,
|
||||||
against_account=None,
|
against_account=None,
|
||||||
|
account_currency=get_account_currency(row.expense_account),
|
||||||
)
|
)
|
||||||
|
|
||||||
if warehouse_with_no_account:
|
if warehouse_with_no_account:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from frappe.utils import add_days, cint, flt, nowtime, today
|
|||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
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.sales_and_purchase_return import make_return_doc
|
||||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||||
get_rm_items,
|
get_rm_items,
|
||||||
@@ -351,26 +352,15 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
|||||||
self.assertEqual(cint(erpnext.is_perpetual_inventory_enabled(scr.company)), 1)
|
self.assertEqual(cint(erpnext.is_perpetual_inventory_enabled(scr.company)), 1)
|
||||||
|
|
||||||
gl_entries = get_gl_entries("Subcontracting Receipt", scr.name)
|
gl_entries = get_gl_entries("Subcontracting Receipt", scr.name)
|
||||||
|
|
||||||
self.assertTrue(gl_entries)
|
self.assertTrue(gl_entries)
|
||||||
|
|
||||||
fg_warehouse_ac = get_inventory_account(scr.company, scr.items[0].warehouse)
|
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
|
expense_account = scr.items[0].expense_account
|
||||||
|
expected_values = {
|
||||||
if fg_warehouse_ac == supplier_warehouse_ac:
|
fg_warehouse_ac: [2100.0, 1000],
|
||||||
expected_values = {
|
expense_account: [1100, 2100],
|
||||||
fg_warehouse_ac: [2100.0, 1000.0], # FG Amount (D), RM Cost (C)
|
additional_costs_expense_account: [0.0, 100.0],
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
for gle in gl_entries:
|
for gle in gl_entries:
|
||||||
self.assertEqual(expected_values[gle.account][0], gle.debit)
|
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))
|
self.assertTrue(get_gl_entries("Subcontracting Receipt", scr.name))
|
||||||
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
|
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):
|
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 as "Material Transferred for Subcontracting" to transfer RM's more than the required qty
|
||||||
set_backflush_based_on("Material Transferred for Subcontract")
|
set_backflush_based_on("Material Transferred for Subcontract")
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<a href="https://erpnext.com?source=website_footer" target="_blank" class="text-muted">Powered by ERPNext</a>
|
{{ _("Powered by {0}").format('<a href="https://erpnext.com?source=website_footer" target="_blank" class="text-muted">ERPNext</a>') }}
|
||||||
|
|||||||
Reference in New Issue
Block a user