mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-23 11:09:41 +00:00
Compare commits
220 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
616448f9c9 | ||
|
|
70a8c64ea4 | ||
|
|
00a75d4984 | ||
|
|
8b826c2369 | ||
|
|
210786119d | ||
|
|
85c509ea69 | ||
|
|
6e35b6b6b3 | ||
|
|
94605687c7 | ||
|
|
e27518b2d4 | ||
|
|
cf71178af6 | ||
|
|
002575244f | ||
|
|
aadc83d0d9 | ||
|
|
66f1e2a076 | ||
|
|
1d5935a10f | ||
|
|
d82106cb76 | ||
|
|
03aeac6c9b | ||
|
|
014f5bff5c | ||
|
|
819667ab24 | ||
|
|
aa20e224fb | ||
|
|
dcae024fd2 | ||
|
|
30a6fe55ca | ||
|
|
57f9894f41 | ||
|
|
2c383a69f9 | ||
|
|
b7d76d8fad | ||
|
|
9b5183e0e5 | ||
|
|
0b43d518af | ||
|
|
a6b490e4b3 | ||
|
|
a5d33d5665 | ||
|
|
48c3c80383 | ||
|
|
a9a68f2523 | ||
|
|
f3da431f1f | ||
|
|
14e92d589f | ||
|
|
3d51c1b363 | ||
|
|
825ef415ef | ||
|
|
f6148c4352 | ||
|
|
3c408ff66b | ||
|
|
f6bc9fc505 | ||
|
|
0c7911d0fe | ||
|
|
b8ecc4274f | ||
|
|
a9b7bc1385 | ||
|
|
d062e50f35 | ||
|
|
646de0bec1 | ||
|
|
02de47c673 | ||
|
|
42b38b7998 | ||
|
|
180f406917 | ||
|
|
90803b852e | ||
|
|
4cbde47ac4 | ||
|
|
0b3215cd42 | ||
|
|
b9f9be3d88 | ||
|
|
faeedb01a8 | ||
|
|
a2402ddf52 | ||
|
|
6b29c06511 | ||
|
|
2cedd455d9 | ||
|
|
ba31f685f8 | ||
|
|
507d27d509 | ||
|
|
e27dd64044 | ||
|
|
7f617a54d1 | ||
|
|
7986e69839 | ||
|
|
836545bdb4 | ||
|
|
4fccef0636 | ||
|
|
2b6b0b32a7 | ||
|
|
dac192a210 | ||
|
|
8421e09a95 | ||
|
|
43a723546d | ||
|
|
ea454ef770 | ||
|
|
0d6be69ce5 | ||
|
|
22d7bbb081 | ||
|
|
4b643feb92 | ||
|
|
06f04cad3e | ||
|
|
c3ebc39f3b | ||
|
|
9d06e093a6 | ||
|
|
1c24bedaf6 | ||
|
|
5a0e690307 | ||
|
|
64e6d65c00 | ||
|
|
e7915b31ce | ||
|
|
b4200c5453 | ||
|
|
2f4bfa77b2 | ||
|
|
5eeaaf4757 | ||
|
|
8faabb8110 | ||
|
|
92ee871b79 | ||
|
|
67dad765dc | ||
|
|
25ff9e0a4d | ||
|
|
a230a0d94c | ||
|
|
8fc8aa2dfd | ||
|
|
e0d9a47ff7 | ||
|
|
9d0034f0a4 | ||
|
|
f3d780c5bc | ||
|
|
89d00ee4a2 | ||
|
|
22e63099ee | ||
|
|
06c3839abc | ||
|
|
a7d13d580c | ||
|
|
dd9dc29fe5 | ||
|
|
b3035ec7d4 | ||
|
|
0ecc056a3f | ||
|
|
7a9150aac0 | ||
|
|
388b5fb8f6 | ||
|
|
96a1a4a8b8 | ||
|
|
eb691226d5 | ||
|
|
00e61a531a | ||
|
|
7abc4c4022 | ||
|
|
1ae3fd3e27 | ||
|
|
ade44fbdb4 | ||
|
|
82e8fb8a5e | ||
|
|
f415bc3d14 | ||
|
|
5bc3273460 | ||
|
|
c263e6d67e | ||
|
|
11b5a81170 | ||
|
|
27485b9472 | ||
|
|
0de4477ddf | ||
|
|
235f271729 | ||
|
|
d38fd0779c | ||
|
|
093c6bb17e | ||
|
|
2c98081be6 | ||
|
|
e5c821a822 | ||
|
|
500ffbab09 | ||
|
|
451326fdeb | ||
|
|
4d12ae069e | ||
|
|
4297c58e5e | ||
|
|
b43fbf2a7d | ||
|
|
6cdc7811b8 | ||
|
|
59ac7aeb23 | ||
|
|
4524c74968 | ||
|
|
80ae9820f0 | ||
|
|
5d92f38bbe | ||
|
|
a944eaf054 | ||
|
|
493c8ce04e | ||
|
|
a711cd7a18 | ||
|
|
942aca31f2 | ||
|
|
77ea06dc7d | ||
|
|
360f3b7c25 | ||
|
|
581221f1ec | ||
|
|
e0be83caa5 | ||
|
|
8934adfef4 | ||
|
|
259251dfab | ||
|
|
f8c54f0a9b | ||
|
|
9ce357f3bf | ||
|
|
2d6f4740be | ||
|
|
a3b38d9a91 | ||
|
|
ed2e8c2cc6 | ||
|
|
18eea3425c | ||
|
|
7aaea9f5b6 | ||
|
|
9e7a5bb1bb | ||
|
|
27105501fb | ||
|
|
4fe09fd7e8 | ||
|
|
76a233e1b7 | ||
|
|
bda447f20a | ||
|
|
7db420cf9f | ||
|
|
5ddfe3ce1d | ||
|
|
ff998572e7 | ||
|
|
f364a41490 | ||
|
|
804edad233 | ||
|
|
e533158a1c | ||
|
|
5bcf642dff | ||
|
|
d7822f493b | ||
|
|
5188745c53 | ||
|
|
006a8c71fd | ||
|
|
ba25d334b1 | ||
|
|
dd1020ccfb | ||
|
|
de2f80811f | ||
|
|
12eaa64572 | ||
|
|
da1dcb8b21 | ||
|
|
9dad08274a | ||
|
|
d8d53bc43e | ||
|
|
8966c956d5 | ||
|
|
a518fc080a | ||
|
|
3b36be214f | ||
|
|
20af226947 | ||
|
|
7f26b1d5a2 | ||
|
|
c332fcdeff | ||
|
|
51adea3b89 | ||
|
|
083402e9a8 | ||
|
|
1116a0fc8e | ||
|
|
b15d7e573e | ||
|
|
9207d79ce6 | ||
|
|
33eb72035b | ||
|
|
2c6099e051 | ||
|
|
5f4aaed5a0 | ||
|
|
94a17b6ec2 | ||
|
|
bb901cab88 | ||
|
|
711e06b537 | ||
|
|
c54b65acc2 | ||
|
|
179b8c7c61 | ||
|
|
b107387dda | ||
|
|
9fcca18bf3 | ||
|
|
b98cce8b9a | ||
|
|
bd9655a781 | ||
|
|
1df3b608ec | ||
|
|
62b7a8d21a | ||
|
|
6b31e54891 | ||
|
|
15ae0196c6 | ||
|
|
a06d705d75 | ||
|
|
9e3e3182f2 | ||
|
|
1393100a62 | ||
|
|
945bdabebb | ||
|
|
f07594b863 | ||
|
|
25a382ec24 | ||
|
|
d7c47e8ea5 | ||
|
|
2627fb10ce | ||
|
|
0790db79fd | ||
|
|
0dad2acfc8 | ||
|
|
193042ef07 | ||
|
|
8208a9ec27 | ||
|
|
fbfc6a6f62 | ||
|
|
e9f9eaa2d0 | ||
|
|
08d40ddb9b | ||
|
|
d60ab92082 | ||
|
|
85a0581145 | ||
|
|
2cac05e56c | ||
|
|
10e4e610db | ||
|
|
9ab80cfd6c | ||
|
|
8f2b83d434 | ||
|
|
7f0ebb37da | ||
|
|
1d42c4a305 | ||
|
|
186173a21e | ||
|
|
96c937bf6a | ||
|
|
52aafc9410 | ||
|
|
3232310c0f | ||
|
|
6397c366fd | ||
|
|
f5ef376486 | ||
|
|
c9eeca22f5 |
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "14.86.0"
|
||||
__version__ = "14.92.2"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -3,4 +3,14 @@
|
||||
|
||||
frappe.ui.form.on("Accounts Settings", {
|
||||
refresh: function (frm) {},
|
||||
|
||||
drop_ar_procedures: function (frm) {
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
method: "drop_ar_sql_procedures",
|
||||
callback: function (r) {
|
||||
frappe.show_alert(__("Procedures dropped"), 5);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -77,6 +77,8 @@
|
||||
"receivable_payable_remarks_length",
|
||||
"accounts_receivable_payable_tuning_section",
|
||||
"receivable_payable_fetch_method",
|
||||
"column_break_ntmi",
|
||||
"drop_ar_procedures",
|
||||
"legacy_section",
|
||||
"ignore_is_opening_check_for_reporting"
|
||||
],
|
||||
@@ -488,7 +490,7 @@
|
||||
"fieldname": "receivable_payable_fetch_method",
|
||||
"fieldtype": "Select",
|
||||
"label": "Data Fetch Method",
|
||||
"options": "Buffered Cursor\nUnBuffered Cursor"
|
||||
"options": "Buffered Cursor\nUnBuffered Cursor\nRaw SQL"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounts_receivable_payable_tuning_section",
|
||||
@@ -499,6 +501,17 @@
|
||||
"fieldname": "legacy_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Legacy Fields"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ntmi",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.receivable_payable_fetch_method == \"Raw SQL\"",
|
||||
"description": "Drops existing SQL Procedures and Function setup by Accounts Receivable report",
|
||||
"fieldname": "drop_ar_procedures",
|
||||
"fieldtype": "Button",
|
||||
"label": "Drop Procedures"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
|
||||
@@ -65,3 +65,11 @@ class AccountsSettings(Document):
|
||||
def validate_pending_reposts(self):
|
||||
if self.acc_frozen_upto:
|
||||
check_pending_reposting(self.acc_frozen_upto)
|
||||
|
||||
@frappe.whitelist()
|
||||
def drop_ar_sql_procedures(self):
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR
|
||||
|
||||
frappe.db.sql(f"drop function if exists {InitSQLProceduresForAR.genkey_function_name}")
|
||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}")
|
||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}")
|
||||
|
||||
@@ -6,7 +6,11 @@ import unittest
|
||||
import frappe
|
||||
from frappe.utils import now_datetime, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.budget.budget import BudgetError, get_actual_expense
|
||||
from erpnext.accounts.doctype.budget.budget import (
|
||||
BudgetError,
|
||||
get_accumulated_monthly_budget,
|
||||
get_actual_expense,
|
||||
)
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
@@ -96,6 +100,10 @@ class TestBudget(unittest.TestCase):
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
)
|
||||
|
||||
mr = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Material Request",
|
||||
@@ -109,7 +117,7 @@ class TestBudget(unittest.TestCase):
|
||||
"uom": "_Test UOM",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"schedule_date": nowdate(),
|
||||
"rate": 100000,
|
||||
"rate": accumulated_limit + 1,
|
||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
}
|
||||
|
||||
@@ -448,9 +448,8 @@ def unset_existing_data(company):
|
||||
"Sales Taxes and Charges Template",
|
||||
"Purchase Taxes and Charges Template",
|
||||
]:
|
||||
frappe.db.sql(
|
||||
f'''delete from `tab{doctype}` where `company`="%s"''' % (company) # nosec
|
||||
)
|
||||
dt = frappe.qb.DocType(doctype)
|
||||
frappe.qb.from_(dt).where(dt.company == company).delete().run()
|
||||
|
||||
|
||||
def set_default_accounts(company):
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.tests.utils import change_settings
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
@@ -190,6 +192,31 @@ class TestCostCenterAllocation(unittest.TestCase):
|
||||
coa2.cancel()
|
||||
jv.cancel()
|
||||
|
||||
@change_settings("System Settings", {"rounding_method": "Commercial Rounding"})
|
||||
def test_debit_credit_on_cost_center_allocation_for_commercial_rounding(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
cca = create_cost_center_allocation(
|
||||
"_Test Company",
|
||||
"Main Cost Center 1 - _TC",
|
||||
{"Sub Cost Center 2 - _TC": 50, "Sub Cost Center 3 - _TC": 50},
|
||||
)
|
||||
|
||||
si = create_sales_invoice(rate=145.65, cost_center="Main Cost Center 1 - _TC")
|
||||
|
||||
gl_entry = frappe.qb.DocType("GL Entry")
|
||||
gl_entries = (
|
||||
frappe.qb.from_(gl_entry)
|
||||
.select(Sum(gl_entry.credit).as_("cr"), Sum(gl_entry.debit).as_("dr"))
|
||||
.where(gl_entry.voucher_type == "Sales Invoice")
|
||||
.where(gl_entry.voucher_no == si.name)
|
||||
).run(as_dict=1)
|
||||
|
||||
self.assertEqual(gl_entries[0].cr, gl_entries[0].dr)
|
||||
|
||||
si.cancel()
|
||||
cca.cancel()
|
||||
|
||||
|
||||
def create_cost_center_allocation(
|
||||
company,
|
||||
|
||||
@@ -55,46 +55,46 @@ class Dunning(AccountsController):
|
||||
"conversion_rate",
|
||||
"cost_center",
|
||||
]
|
||||
inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1)
|
||||
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
invoice_fields.extend(accounting_dimensions)
|
||||
|
||||
inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1)
|
||||
|
||||
dunning_in_company_currency = flt(self.dunning_amount * inv.conversion_rate)
|
||||
default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": inv.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": self.customer,
|
||||
"due_date": self.due_date,
|
||||
"against": self.income_account,
|
||||
"debit": dunning_in_company_currency,
|
||||
"debit_in_account_currency": self.dunning_amount,
|
||||
"against_voucher": self.name,
|
||||
"against_voucher_type": "Dunning",
|
||||
"cost_center": inv.cost_center or default_cost_center,
|
||||
"project": inv.project,
|
||||
},
|
||||
inv.party_account_currency,
|
||||
item=inv,
|
||||
)
|
||||
)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.income_account,
|
||||
"against": self.customer,
|
||||
"credit": dunning_in_company_currency,
|
||||
"cost_center": inv.cost_center or default_cost_center,
|
||||
"credit_in_account_currency": self.dunning_amount,
|
||||
"project": inv.project,
|
||||
},
|
||||
item=inv,
|
||||
)
|
||||
)
|
||||
debit = {
|
||||
"account": inv.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": self.customer,
|
||||
"due_date": self.due_date,
|
||||
"against": self.income_account,
|
||||
"debit": dunning_in_company_currency,
|
||||
"debit_in_account_currency": self.dunning_amount,
|
||||
"against_voucher": self.name,
|
||||
"against_voucher_type": "Dunning",
|
||||
"cost_center": inv.cost_center or default_cost_center,
|
||||
"project": inv.project,
|
||||
}
|
||||
|
||||
credit = {
|
||||
"account": self.income_account,
|
||||
"against": self.customer,
|
||||
"credit": dunning_in_company_currency,
|
||||
"credit_in_account_currency": self.dunning_amount,
|
||||
"cost_center": inv.cost_center or default_cost_center,
|
||||
"project": inv.project,
|
||||
}
|
||||
|
||||
for dimension in accounting_dimensions:
|
||||
if val := inv.get(dimension):
|
||||
debit[dimension] = credit[dimension] = val
|
||||
|
||||
gl_entries = [
|
||||
self.get_gl_dict(debit, inv.party_account_currency, item=inv),
|
||||
self.get_gl_dict(credit, item=inv),
|
||||
]
|
||||
make_gl_entries(
|
||||
gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False
|
||||
)
|
||||
|
||||
@@ -261,7 +261,7 @@ def validate_balance_type(account, adv_adj=False):
|
||||
if balance_must_be:
|
||||
balance = frappe.db.sql(
|
||||
"""select sum(debit) - sum(credit)
|
||||
from `tabGL Entry` where account = %s""",
|
||||
from `tabGL Entry` where is_cancelled = 0 and account = %s""",
|
||||
account,
|
||||
)[0][0]
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import flt, today
|
||||
|
||||
|
||||
@@ -18,22 +19,30 @@ def get_loyalty_details(
|
||||
if not expiry_date:
|
||||
expiry_date = today()
|
||||
|
||||
condition = ""
|
||||
if company:
|
||||
condition = " and company=%s " % frappe.db.escape(company)
|
||||
if not include_expired_entry:
|
||||
condition += " and expiry_date>='%s' " % expiry_date
|
||||
LoyaltyPointEntry = frappe.qb.DocType("Loyalty Point Entry")
|
||||
|
||||
loyalty_point_details = frappe.db.sql(
|
||||
f"""select sum(loyalty_points) as loyalty_points,
|
||||
sum(purchase_amount) as total_spent from `tabLoyalty Point Entry`
|
||||
where customer=%s and loyalty_program=%s and posting_date <= %s
|
||||
{condition}
|
||||
group by customer""",
|
||||
(customer, loyalty_program, expiry_date),
|
||||
as_dict=1,
|
||||
query = (
|
||||
frappe.qb.from_(LoyaltyPointEntry)
|
||||
.select(
|
||||
Sum(LoyaltyPointEntry.loyalty_points).as_("loyalty_points"),
|
||||
Sum(LoyaltyPointEntry.purchase_amount).as_("total_spent"),
|
||||
)
|
||||
.where(
|
||||
(LoyaltyPointEntry.customer == customer)
|
||||
& (LoyaltyPointEntry.loyalty_program == loyalty_program)
|
||||
& (LoyaltyPointEntry.posting_date <= expiry_date)
|
||||
)
|
||||
.groupby(LoyaltyPointEntry.customer)
|
||||
)
|
||||
|
||||
if company:
|
||||
query = query.where(LoyaltyPointEntry.company == company)
|
||||
|
||||
if not include_expired_entry:
|
||||
query = query.where(LoyaltyPointEntry.expiry_date >= expiry_date)
|
||||
|
||||
loyalty_point_details = query.run(as_dict=True)
|
||||
|
||||
if loyalty_point_details:
|
||||
return loyalty_point_details[0]
|
||||
else:
|
||||
|
||||
@@ -60,6 +60,6 @@ def create_party_link(primary_role, primary_party, secondary_party):
|
||||
party_link.secondary_role = "Customer" if primary_role == "Supplier" else "Supplier"
|
||||
party_link.secondary_party = secondary_party
|
||||
|
||||
party_link.save(ignore_permissions=True)
|
||||
party_link.save()
|
||||
|
||||
return party_link
|
||||
|
||||
@@ -330,7 +330,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
payment_type: function(frm) {
|
||||
set_default_party_type(frm);
|
||||
if(frm.doc.payment_type == "Internal Transfer") {
|
||||
$.each(["party", "party_balance", "paid_from", "paid_to",
|
||||
$.each(["party", "party_type", "party_balance", "paid_from", "paid_to",
|
||||
"references", "total_allocated_amount"], function(i, field) {
|
||||
frm.set_value(field, null);
|
||||
});
|
||||
|
||||
@@ -10,14 +10,19 @@
|
||||
"description",
|
||||
"section_break_4",
|
||||
"due_date",
|
||||
"invoice_portion",
|
||||
"mode_of_payment",
|
||||
"column_break_5",
|
||||
"invoice_portion",
|
||||
"due_date_based_on",
|
||||
"credit_days",
|
||||
"credit_months",
|
||||
"section_break_6",
|
||||
"discount_type",
|
||||
"discount_date",
|
||||
"column_break_9",
|
||||
"discount",
|
||||
"discount_type",
|
||||
"column_break_9",
|
||||
"discount_validity_based_on",
|
||||
"discount_validity",
|
||||
"section_break_9",
|
||||
"payment_amount",
|
||||
"outstanding",
|
||||
@@ -155,12 +160,50 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Payment Amount (Company Currency)",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "due_date_based_on",
|
||||
"fieldtype": "Select",
|
||||
"label": "Due Date Based On",
|
||||
"options": "\nDay(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)",
|
||||
"fieldname": "credit_days",
|
||||
"fieldtype": "Int",
|
||||
"label": "Credit Days",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'",
|
||||
"fieldname": "credit_months",
|
||||
"fieldtype": "Int",
|
||||
"label": "Credit Months",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "discount",
|
||||
"fieldname": "discount_validity_based_on",
|
||||
"fieldtype": "Select",
|
||||
"label": "Discount Validity Based On",
|
||||
"options": "\nDay(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "discount_validity_based_on",
|
||||
"fieldname": "discount_validity",
|
||||
"fieldtype": "Int",
|
||||
"label": "Discount Validity",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-09-16 13:57:06.382859",
|
||||
"modified": "2025-07-31 08:38:25.820701",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Schedule",
|
||||
@@ -171,4 +214,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,4 +161,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,6 +206,9 @@ class PeriodClosingVoucher(AccountsController):
|
||||
return gl_entry
|
||||
|
||||
def get_gle_for_closing_account(self, acc):
|
||||
debit = abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0
|
||||
credit = abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0
|
||||
|
||||
gl_entry = self.get_gl_dict(
|
||||
{
|
||||
"company": self.company,
|
||||
@@ -214,16 +217,10 @@ class PeriodClosingVoucher(AccountsController):
|
||||
"cost_center": acc.cost_center,
|
||||
"finance_book": acc.finance_book,
|
||||
"account_currency": acc.account_currency,
|
||||
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) > 0
|
||||
else 0,
|
||||
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
|
||||
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) < 0
|
||||
else 0,
|
||||
"credit": abs(flt(acc.bal_in_company_currency))
|
||||
if flt(acc.bal_in_company_currency) < 0
|
||||
else 0,
|
||||
"debit_in_account_currency": debit,
|
||||
"debit": debit,
|
||||
"credit_in_account_currency": credit,
|
||||
"credit": credit,
|
||||
"is_period_closing_voucher_entry": 1,
|
||||
},
|
||||
item=acc,
|
||||
|
||||
@@ -560,7 +560,13 @@ class POSInvoice(SalesInvoice):
|
||||
"Account", self.debit_to, "account_currency", cache=True
|
||||
)
|
||||
if not self.due_date and self.customer:
|
||||
self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company)
|
||||
self.due_date = get_due_date(
|
||||
self.posting_date,
|
||||
"Customer",
|
||||
self.customer,
|
||||
self.company,
|
||||
template_name=self.payment_terms_template,
|
||||
)
|
||||
|
||||
super(SalesInvoice, self).set_missing_values(for_validate)
|
||||
|
||||
|
||||
@@ -29,7 +29,10 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
this.frm.set_query("expense_account", "items", function () {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_expense_account",
|
||||
filters: { company: doc.company },
|
||||
filters: {
|
||||
company: doc.company,
|
||||
disabled: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -173,7 +173,12 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
if not self.due_date:
|
||||
self.due_date = get_due_date(
|
||||
self.posting_date, "Supplier", self.supplier, self.company, self.bill_date
|
||||
self.posting_date,
|
||||
"Supplier",
|
||||
self.supplier,
|
||||
self.company,
|
||||
self.bill_date,
|
||||
template_name=self.payment_terms_template,
|
||||
)
|
||||
|
||||
tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
|
||||
@@ -781,7 +786,7 @@ class PurchaseInvoice(BuyingController):
|
||||
self.get_provisional_accounts()
|
||||
|
||||
for item in self.get("items"):
|
||||
if flt(item.base_net_amount):
|
||||
if flt(item.base_net_amount) or (self.get("update_stock") and item.valuation_rate):
|
||||
account_currency = get_account_currency(item.expense_account)
|
||||
if item.item_code:
|
||||
frappe.get_cached_value("Item", item.item_code, "asset_category")
|
||||
@@ -1016,6 +1021,9 @@ class PurchaseInvoice(BuyingController):
|
||||
def get_provisional_accounts(self):
|
||||
self.provisional_accounts = frappe._dict()
|
||||
linked_purchase_receipts = set([d.purchase_receipt for d in self.items if d.purchase_receipt])
|
||||
if not linked_purchase_receipts:
|
||||
return
|
||||
|
||||
pr_items = frappe.get_all(
|
||||
"Purchase Receipt Item",
|
||||
filters={"parent": ("in", linked_purchase_receipts)},
|
||||
@@ -1124,6 +1132,30 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
warehouse_debit_amount = stock_amount
|
||||
|
||||
elif self.is_return and self.update_stock and (self.is_internal_supplier or not self.return_against):
|
||||
net_rate = item.base_net_amount
|
||||
stock_amount = net_rate + item.item_tax_amount + flt(item.landed_cost_voucher_amount)
|
||||
|
||||
if flt(stock_amount, net_amt_precision) != flt(warehouse_debit_amount, net_amt_precision):
|
||||
cost_of_goods_sold_account = self.get_company_default("default_expense_account")
|
||||
stock_adjustment_amt = stock_amount - warehouse_debit_amount
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": cost_of_goods_sold_account,
|
||||
"against": item.expense_account,
|
||||
"debit": stock_adjustment_amt,
|
||||
"debit_in_transaction_currency": stock_adjustment_amt / self.conversion_rate,
|
||||
"remarks": self.get("remarks") or _("Stock Adjustment"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
return warehouse_debit_amount
|
||||
|
||||
def make_tax_gl_entries(self, gl_entries):
|
||||
|
||||
@@ -1816,19 +1816,16 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
|
||||
self.assertAlmostEqual(rate, 500)
|
||||
|
||||
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
|
||||
def test_payment_allocation_for_payment_terms(self):
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
create_pr_against_po,
|
||||
create_purchase_order,
|
||||
)
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import (
|
||||
automatically_fetch_payment_terms,
|
||||
)
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
make_purchase_invoice as make_pi_from_pr,
|
||||
)
|
||||
|
||||
automatically_fetch_payment_terms()
|
||||
frappe.db.set_value(
|
||||
"Payment Terms Template",
|
||||
"_Test Payment Term Template",
|
||||
@@ -1854,7 +1851,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
pi = make_pi_from_pr(pr.name)
|
||||
self.assertEqual(pi.payment_schedule[0].payment_amount, 1000)
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
frappe.db.set_value(
|
||||
"Payment Terms Template",
|
||||
"_Test Payment Term Template",
|
||||
|
||||
@@ -151,7 +151,7 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
if doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
doc.docstatus = 2
|
||||
doc.make_gl_entries_on_cancel()
|
||||
doc.make_gl_entries_on_cancel(from_repost=True)
|
||||
|
||||
doc.docstatus = 1
|
||||
if doc.doctype == "Sales Invoice":
|
||||
@@ -163,7 +163,7 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
elif doc.doctype == "Purchase Receipt":
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
doc.docstatus = 2
|
||||
doc.make_gl_entries_on_cancel()
|
||||
doc.make_gl_entries_on_cancel(from_repost=True)
|
||||
|
||||
doc.docstatus = 1
|
||||
doc.make_gl_entries(from_repost=True)
|
||||
|
||||
@@ -498,7 +498,13 @@ class SalesInvoice(SellingController):
|
||||
"Account", self.debit_to, "account_currency", cache=True
|
||||
)
|
||||
if not self.due_date and self.customer:
|
||||
self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company)
|
||||
self.due_date = get_due_date(
|
||||
self.posting_date,
|
||||
"Customer",
|
||||
self.customer,
|
||||
self.company,
|
||||
template_name=self.payment_terms_template,
|
||||
)
|
||||
|
||||
super().set_missing_values(for_validate)
|
||||
|
||||
@@ -1019,7 +1025,6 @@ class SalesInvoice(SellingController):
|
||||
|
||||
self.make_exchange_gain_loss_journal()
|
||||
elif self.docstatus == 2:
|
||||
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
|
||||
if update_outstanding == "No":
|
||||
|
||||
@@ -3287,6 +3287,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
si.posting_date = getdate()
|
||||
si.submit()
|
||||
|
||||
@change_settings("Accounts Settings", {"over_billing_allowance": 0})
|
||||
def test_over_billing_case_against_delivery_note(self):
|
||||
"""
|
||||
Test a case where duplicating the item with qty = 1 in the invoice
|
||||
@@ -3294,24 +3295,23 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
"""
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
||||
frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", 0)
|
||||
|
||||
dn = create_delivery_note()
|
||||
dn.submit()
|
||||
|
||||
si = make_sales_invoice(dn.name)
|
||||
# make a copy of first item and add it to invoice
|
||||
item_copy = frappe.copy_doc(si.items[0])
|
||||
si.save()
|
||||
|
||||
si.items = [] # Clear existing items
|
||||
si.append("items", item_copy)
|
||||
si.save()
|
||||
|
||||
si.append("items", item_copy)
|
||||
with self.assertRaises(frappe.ValidationError) as err:
|
||||
si.submit()
|
||||
si.save()
|
||||
|
||||
self.assertTrue("cannot overbill" in str(err.exception).lower())
|
||||
|
||||
frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", over_billing_allowance)
|
||||
dn.cancel()
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
@@ -3889,6 +3889,60 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
self.assertEqual(invoice.outstanding_amount, 0)
|
||||
|
||||
def test_system_generated_exchange_gain_or_loss_je_after_repost(self):
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.test_repost_accounting_ledger import (
|
||||
update_repost_settings,
|
||||
)
|
||||
|
||||
update_repost_settings()
|
||||
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
debit_to="_Test Receivable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=80,
|
||||
)
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name)
|
||||
pe.reference_no = "10"
|
||||
pe.reference_date = nowdate()
|
||||
pe.paid_from_account_currency = si.currency
|
||||
pe.paid_to_account_currency = "INR"
|
||||
pe.source_exchange_rate = 85
|
||||
pe.target_exchange_rate = 1
|
||||
pe.paid_amount = si.outstanding_amount
|
||||
pe.received_amount = 8500
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = si.company
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.save()
|
||||
ral.submit()
|
||||
|
||||
je = frappe.qb.DocType("Journal Entry")
|
||||
jea = frappe.qb.DocType("Journal Entry Account")
|
||||
q = (
|
||||
(
|
||||
frappe.qb.from_(je)
|
||||
.join(jea)
|
||||
.on(je.name == jea.parent)
|
||||
.select(je.docstatus)
|
||||
.where(
|
||||
(je.voucher_type == "Exchange Gain Or Loss")
|
||||
& (jea.reference_name == si.name)
|
||||
& (jea.reference_type == "Sales Invoice")
|
||||
& (je.is_system_generated == 1)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
.run()
|
||||
)
|
||||
|
||||
self.assertEqual(q[0][0], 1)
|
||||
|
||||
|
||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
gl_entries = frappe.db.sql(
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Even invoices with apply tax withholding unchecked will be considered for checking cumulative threshold breach",
|
||||
"description": "Only payment entries with apply tax withholding unchecked will be considered for checking cumulative threshold breach",
|
||||
"fieldname": "consider_party_ledger_amount",
|
||||
"fieldtype": "Check",
|
||||
"label": "Consider Entire Party Ledger Amount",
|
||||
@@ -102,10 +102,11 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-07-27 21:47:34.396071",
|
||||
"modified": "2025-07-30 07:13:51.785735",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withholding Category",
|
||||
"naming_rule": "Set by user",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -148,4 +149,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +179,15 @@ def process_gl_map(gl_map, merge_entries=True, precision=None):
|
||||
|
||||
|
||||
def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
|
||||
round_off_account, default_currency = frappe.get_cached_value(
|
||||
"Company", gl_map[0].company, ["round_off_account", "default_currency"]
|
||||
)
|
||||
if not precision:
|
||||
precision = get_field_precision(
|
||||
frappe.get_meta("GL Entry").get_field("debit"),
|
||||
currency=default_currency,
|
||||
)
|
||||
|
||||
new_gl_map = []
|
||||
for d in gl_map:
|
||||
cost_center = d.get("cost_center")
|
||||
@@ -192,6 +201,11 @@ def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
|
||||
new_gl_map.append(d)
|
||||
continue
|
||||
|
||||
if d.account == round_off_account:
|
||||
d.cost_center = cost_center_allocation[0][0]
|
||||
new_gl_map.append(d)
|
||||
continue
|
||||
|
||||
for sub_cost_center, percentage in cost_center_allocation:
|
||||
gle = copy.deepcopy(d)
|
||||
gle.cost_center = sub_cost_center
|
||||
|
||||
@@ -552,12 +552,13 @@ def validate_party_accounts(doc):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_due_date(posting_date, party_type, party, company=None, bill_date=None):
|
||||
def get_due_date(posting_date, party_type, party, company=None, bill_date=None, template_name=None):
|
||||
"""Get due date from `Payment Terms Template`"""
|
||||
due_date = None
|
||||
if (bill_date or posting_date) and party:
|
||||
due_date = bill_date or posting_date
|
||||
template_name = get_payment_terms_template(party, party_type, company)
|
||||
if not template_name:
|
||||
template_name = get_payment_terms_template(party, party_type, company)
|
||||
|
||||
if template_name:
|
||||
due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime("%Y-%m-%d")
|
||||
|
||||
@@ -6,6 +6,7 @@ from collections import OrderedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb, query_builder, scrub
|
||||
from frappe.database.schema import get_definition
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Date, Substring, Sum
|
||||
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||
@@ -14,7 +15,10 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
get_dimension_with_children,
|
||||
)
|
||||
from erpnext.accounts.utils import get_currency_precision
|
||||
from erpnext.accounts.utils import (
|
||||
build_qb_match_conditions,
|
||||
get_currency_precision,
|
||||
)
|
||||
|
||||
# This report gives a summary of all Outstanding Invoices considering the following
|
||||
|
||||
@@ -91,9 +95,6 @@ class ReceivablePayableReport:
|
||||
def get_data(self):
|
||||
self.get_sales_invoices_or_customers_based_on_sales_person()
|
||||
|
||||
# Build delivery note map against all sales invoices
|
||||
self.build_delivery_note_map()
|
||||
|
||||
# Get invoice details like bill_no, due_date etc for all invoices
|
||||
self.get_invoice_details()
|
||||
|
||||
@@ -114,12 +115,16 @@ class ReceivablePayableReport:
|
||||
self.fetch_ple_in_buffered_cursor()
|
||||
elif self.ple_fetch_method == "UnBuffered Cursor":
|
||||
self.fetch_ple_in_unbuffered_cursor()
|
||||
elif self.ple_fetch_method == "Raw SQL":
|
||||
self.fetch_ple_in_sql_procedures()
|
||||
|
||||
# Build delivery note map against all sales invoices
|
||||
self.build_delivery_note_map()
|
||||
|
||||
self.build_data()
|
||||
|
||||
def fetch_ple_in_buffered_cursor(self):
|
||||
query, param = self.ple_query.walk()
|
||||
self.ple_entries = frappe.db.sql(query, param, as_dict=True)
|
||||
self.ple_entries = self.ple_query.run(as_dict=True)
|
||||
|
||||
for ple in self.ple_entries:
|
||||
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
|
||||
@@ -132,9 +137,8 @@ class ReceivablePayableReport:
|
||||
|
||||
def fetch_ple_in_unbuffered_cursor(self):
|
||||
self.ple_entries = []
|
||||
query, param = self.ple_query.walk()
|
||||
with frappe.db.unbuffered_cursor():
|
||||
for ple in frappe.db.sql(query, param, as_dict=True, as_iterator=True):
|
||||
for ple in self.ple_query.run(as_dict=True, as_iterator=True):
|
||||
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
|
||||
self.ple_entries.append(ple)
|
||||
|
||||
@@ -299,6 +303,79 @@ class ReceivablePayableReport:
|
||||
row.paid -= amount
|
||||
row.paid_in_account_currency -= amount_in_account_currency
|
||||
|
||||
def fetch_ple_in_sql_procedures(self):
|
||||
self.proc = InitSQLProceduresForAR()
|
||||
|
||||
build_balance = f"""
|
||||
begin not atomic
|
||||
declare done boolean default false;
|
||||
declare rec1 row type of `{self.proc._row_def_table_name}`;
|
||||
declare ple cursor for {self.ple_query.get_sql()};
|
||||
declare continue handler for not found set done = true;
|
||||
|
||||
open ple;
|
||||
fetch ple into rec1;
|
||||
while not done do
|
||||
call {self.proc.init_procedure_name}(rec1);
|
||||
fetch ple into rec1;
|
||||
end while;
|
||||
close ple;
|
||||
|
||||
set done = false;
|
||||
open ple;
|
||||
fetch ple into rec1;
|
||||
while not done do
|
||||
call {self.proc.allocate_procedure_name}(rec1);
|
||||
fetch ple into rec1;
|
||||
end while;
|
||||
close ple;
|
||||
end;
|
||||
"""
|
||||
frappe.db.sql(build_balance)
|
||||
|
||||
balances = frappe.db.sql(
|
||||
f"""select
|
||||
name,
|
||||
voucher_type,
|
||||
voucher_no,
|
||||
party,
|
||||
party_account `account`,
|
||||
posting_date,
|
||||
account_currency,
|
||||
cost_center,
|
||||
sum(invoiced) `invoiced`,
|
||||
sum(paid) `paid`,
|
||||
sum(credit_note) `credit_note`,
|
||||
sum(invoiced) - sum(paid) - sum(credit_note) `outstanding`,
|
||||
sum(invoiced_in_account_currency) `invoiced_in_account_currency`,
|
||||
sum(paid_in_account_currency) `paid_in_account_currency`,
|
||||
sum(credit_note_in_account_currency) `credit_note_in_account_currency`,
|
||||
sum(invoiced_in_account_currency) - sum(paid_in_account_currency) - sum(credit_note_in_account_currency) `outstanding_in_account_currency`
|
||||
from `{self.proc._voucher_balance_name}` group by name order by posting_date;""",
|
||||
as_dict=True,
|
||||
)
|
||||
for x in balances:
|
||||
if self.filters.get("ignore_accounts"):
|
||||
key = (x.voucher_type, x.voucher_no, x.party)
|
||||
else:
|
||||
key = (x.account, x.voucher_type, x.voucher_no, x.party)
|
||||
|
||||
_d = self.build_voucher_dict(x)
|
||||
for field in [
|
||||
"invoiced",
|
||||
"paid",
|
||||
"credit_note",
|
||||
"outstanding",
|
||||
"invoiced_in_account_currency",
|
||||
"paid_in_account_currency",
|
||||
"credit_note_in_account_currency",
|
||||
"outstanding_in_account_currency",
|
||||
"cost_center",
|
||||
]:
|
||||
_d[field] = x.get(field)
|
||||
|
||||
self.voucher_balance[key] = _d
|
||||
|
||||
def update_sub_total_row(self, row, party):
|
||||
total_row = self.total_row_map.get(party)
|
||||
|
||||
@@ -839,6 +916,9 @@ class ReceivablePayableReport:
|
||||
else:
|
||||
query = query.select(ple.remarks)
|
||||
|
||||
if match_conditions := build_qb_match_conditions("Payment Ledger Entry"):
|
||||
query = query.where(Criterion.all(match_conditions))
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
query = query.orderby(self.ple.party, self.ple.posting_date)
|
||||
else:
|
||||
@@ -1231,3 +1311,134 @@ def get_customer_group_with_children(customer_groups):
|
||||
frappe.throw(_("Customer Group: {0} does not exist").format(d))
|
||||
|
||||
return list(set(all_customer_groups))
|
||||
|
||||
|
||||
class InitSQLProceduresForAR:
|
||||
"""
|
||||
Initialize SQL Procedures, Functions and Temporary tables to build Receivable / Payable report
|
||||
"""
|
||||
|
||||
_varchar_type = get_definition("Data")
|
||||
_currency_type = get_definition("Currency")
|
||||
# Temporary Tables
|
||||
_voucher_balance_name = "_ar_voucher_balance"
|
||||
_voucher_balance_definition = f"""
|
||||
create temporary table `{_voucher_balance_name}`(
|
||||
name {_varchar_type},
|
||||
voucher_type {_varchar_type},
|
||||
voucher_no {_varchar_type},
|
||||
party {_varchar_type},
|
||||
party_account {_varchar_type},
|
||||
posting_date date,
|
||||
account_currency {_varchar_type},
|
||||
cost_center {_varchar_type},
|
||||
invoiced {_currency_type},
|
||||
paid {_currency_type},
|
||||
credit_note {_currency_type},
|
||||
invoiced_in_account_currency {_currency_type},
|
||||
paid_in_account_currency {_currency_type},
|
||||
credit_note_in_account_currency {_currency_type}) engine=memory;
|
||||
"""
|
||||
|
||||
_row_def_table_name = "_ar_ple_row"
|
||||
_row_def_table_definition = f"""
|
||||
create temporary table `{_row_def_table_name}`(
|
||||
name {_varchar_type},
|
||||
account {_varchar_type},
|
||||
voucher_type {_varchar_type},
|
||||
voucher_no {_varchar_type},
|
||||
against_voucher_type {_varchar_type},
|
||||
against_voucher_no {_varchar_type},
|
||||
party_type {_varchar_type},
|
||||
cost_center {_varchar_type},
|
||||
party {_varchar_type},
|
||||
posting_date date,
|
||||
due_date date,
|
||||
account_currency {_varchar_type},
|
||||
amount {_currency_type},
|
||||
amount_in_account_currency {_currency_type}) engine=memory;
|
||||
"""
|
||||
|
||||
# Function
|
||||
genkey_function_name = "ar_genkey"
|
||||
genkey_function_sql = f"""
|
||||
create function `{genkey_function_name}`(rec row type of `{_row_def_table_name}`, allocate bool) returns char(40)
|
||||
begin
|
||||
if allocate then
|
||||
return sha1(concat_ws(',', rec.account, rec.against_voucher_type, rec.against_voucher_no, rec.party));
|
||||
else
|
||||
return sha1(concat_ws(',', rec.account, rec.voucher_type, rec.voucher_no, rec.party));
|
||||
end if;
|
||||
end
|
||||
"""
|
||||
|
||||
# Procedures
|
||||
init_procedure_name = "ar_init_tmp_table"
|
||||
init_procedure_sql = f"""
|
||||
create procedure ar_init_tmp_table(in ple row type of `{_row_def_table_name}`)
|
||||
begin
|
||||
if not exists (select name from `{_voucher_balance_name}` where name = `{genkey_function_name}`(ple, false))
|
||||
then
|
||||
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, 0, 0, 0, 0, 0, 0);
|
||||
end if;
|
||||
end;
|
||||
"""
|
||||
|
||||
allocate_procedure_name = "ar_allocate_to_tmp_table"
|
||||
allocate_procedure_sql = f"""
|
||||
create procedure ar_allocate_to_tmp_table(in ple row type of `{_row_def_table_name}`)
|
||||
begin
|
||||
declare invoiced {_currency_type} default 0;
|
||||
declare invoiced_in_account_currency {_currency_type} default 0;
|
||||
declare paid {_currency_type} default 0;
|
||||
declare paid_in_account_currency {_currency_type} default 0;
|
||||
declare credit_note {_currency_type} default 0;
|
||||
declare credit_note_in_account_currency {_currency_type} default 0;
|
||||
|
||||
|
||||
if ple.amount > 0 then
|
||||
if (ple.voucher_type in ("Journal Entry", "Payment Entry") and (ple.voucher_no != ple.against_voucher_no)) then
|
||||
set paid = -1 * ple.amount;
|
||||
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
else
|
||||
set invoiced = ple.amount;
|
||||
set invoiced_in_account_currency = ple.amount_in_account_currency;
|
||||
end if;
|
||||
else
|
||||
|
||||
if ple.voucher_type in ("Sales Invoice", "Purchase Invoice") then
|
||||
if (ple.voucher_no = ple.against_voucher_no) then
|
||||
set paid = -1 * ple.amount;
|
||||
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
else
|
||||
set credit_note = -1 * ple.amount;
|
||||
set credit_note_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
end if;
|
||||
else
|
||||
set paid = -1 * ple.amount;
|
||||
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
end if;
|
||||
|
||||
end if;
|
||||
|
||||
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, true), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0);
|
||||
end;
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
existing_procedures = frappe.db.get_routines()
|
||||
|
||||
if self.genkey_function_name not in existing_procedures:
|
||||
frappe.db.sql(self.genkey_function_sql)
|
||||
|
||||
if self.init_procedure_name not in existing_procedures:
|
||||
frappe.db.sql(self.init_procedure_sql)
|
||||
|
||||
if self.allocate_procedure_name not in existing_procedures:
|
||||
frappe.db.sql(self.allocate_procedure_sql)
|
||||
|
||||
frappe.db.sql(f"drop table if exists `{self._voucher_balance_name}`")
|
||||
frappe.db.sql(self._voucher_balance_definition)
|
||||
|
||||
frappe.db.sql(f"drop table if exists `{self._row_def_table_name}`")
|
||||
frappe.db.sql(self._row_def_table_definition)
|
||||
|
||||
@@ -197,6 +197,11 @@ frappe.query_reports["General Ledger"] = {
|
||||
label: __("Show Net Values in Party Account"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "show_amount_in_company_currency",
|
||||
label: __("Show Credit / Debit in Company Currency"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "show_remarks",
|
||||
label: __("Show Remarks"),
|
||||
|
||||
@@ -205,7 +205,7 @@ def get_gl_entries(filters, accounting_dimensions):
|
||||
)
|
||||
|
||||
if filters.get("presentation_currency"):
|
||||
return convert_to_presentation_currency(gl_entries, currency_map)
|
||||
return convert_to_presentation_currency(gl_entries, currency_map, filters)
|
||||
else:
|
||||
return gl_entries
|
||||
|
||||
@@ -580,6 +580,19 @@ def get_columns(filters):
|
||||
company = filters.get("company") or get_default_company()
|
||||
filters["presentation_currency"] = currency = get_company_currency(company)
|
||||
|
||||
company_currency = get_company_currency(filters.get("company") or get_default_company())
|
||||
|
||||
if (
|
||||
filters.get("show_amount_in_company_currency")
|
||||
and filters["presentation_currency"] != company_currency
|
||||
):
|
||||
frappe.throw(
|
||||
_("Presentation Currency cannot be {0} , When {1} is enabled.").format(
|
||||
frappe.bold(filters["presentation_currency"]),
|
||||
frappe.bold("Show Credit / Debit in Company Currency"),
|
||||
)
|
||||
)
|
||||
|
||||
columns = [
|
||||
{
|
||||
"label": _("GL Entry"),
|
||||
|
||||
@@ -46,6 +46,7 @@ class PaymentLedger:
|
||||
against_voucher_no=ple.against_voucher_no,
|
||||
amount=ple.amount,
|
||||
currency=ple.account_currency,
|
||||
company=ple.company,
|
||||
)
|
||||
|
||||
if self.filters.include_account_currency:
|
||||
@@ -77,6 +78,7 @@ class PaymentLedger:
|
||||
against_voucher_no="Outstanding:",
|
||||
amount=total,
|
||||
currency=voucher_data[0].currency,
|
||||
company=voucher_data[0].company,
|
||||
)
|
||||
|
||||
if self.filters.include_account_currency:
|
||||
@@ -85,7 +87,12 @@ class PaymentLedger:
|
||||
voucher_data.append(entry)
|
||||
|
||||
# empty row
|
||||
voucher_data.append(frappe._dict())
|
||||
voucher_data.append(
|
||||
frappe._dict(
|
||||
currency=voucher_data[0].currency,
|
||||
company=voucher_data[0].company,
|
||||
)
|
||||
)
|
||||
self.data.extend(voucher_data)
|
||||
|
||||
def build_conditions(self):
|
||||
|
||||
@@ -45,6 +45,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
gle_map = get_gle_map(tds_docs)
|
||||
|
||||
out = []
|
||||
entries = {}
|
||||
for name, details in gle_map.items():
|
||||
for entry in details:
|
||||
tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0
|
||||
@@ -119,9 +120,14 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
"supplier_invoice_date": bill_date,
|
||||
}
|
||||
)
|
||||
out.append(row)
|
||||
|
||||
out.sort(key=lambda x: x["section_code"])
|
||||
key = entry.voucher_no
|
||||
if key in entries:
|
||||
entries[key]["tax_amount"] += tax_amount
|
||||
else:
|
||||
entries[key] = row
|
||||
out = list(entries.values())
|
||||
out.sort(key=lambda x: (x["section_code"], x["transaction_date"]))
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@@ -67,11 +67,12 @@ class TestTdsPayableMonthly(AccountsTestMixin, FrappeTestCase):
|
||||
mid_year = add_to_date(fiscal_year[1], months=6)
|
||||
tds_doc = frappe.get_doc("Tax Withholding Category", "TDS - 3")
|
||||
tds_doc.rates[0].to_date = mid_year
|
||||
from_date = add_to_date(mid_year, days=1)
|
||||
tds_doc.append(
|
||||
"rates",
|
||||
{
|
||||
"tax_withholding_rate": 20,
|
||||
"from_date": add_to_date(mid_year, days=1),
|
||||
"from_date": from_date,
|
||||
"to_date": fiscal_year[2],
|
||||
"single_threshold": 1,
|
||||
"cumulative_threshold": 1,
|
||||
@@ -80,18 +81,19 @@ class TestTdsPayableMonthly(AccountsTestMixin, FrappeTestCase):
|
||||
|
||||
tds_doc.save()
|
||||
|
||||
inv_1 = make_purchase_invoice(rate=1000, do_not_submit=True)
|
||||
inv_1 = make_purchase_invoice(
|
||||
rate=1000, posting_date=add_to_date(fiscal_year[1], days=1), do_not_save=True, do_not_submit=True
|
||||
)
|
||||
inv_1.set_posting_time = 1
|
||||
inv_1.apply_tds = 1
|
||||
inv_1.tax_withholding_category = "TDS - 3"
|
||||
inv_1.tax_withholding_category = tds_doc.name
|
||||
inv_1.save()
|
||||
inv_1.submit()
|
||||
|
||||
inv_2 = make_purchase_invoice(
|
||||
rate=1000, do_not_submit=True, posting_date=add_to_date(mid_year, days=1), do_not_save=True
|
||||
)
|
||||
inv_2 = make_purchase_invoice(rate=1000, posting_date=from_date, do_not_save=True, do_not_submit=True)
|
||||
inv_2.set_posting_time = 1
|
||||
|
||||
inv_1.apply_tds = 1
|
||||
inv_2.tax_withholding_category = "TDS - 3"
|
||||
inv_2.apply_tds = 1
|
||||
inv_2.tax_withholding_category = tds_doc.name
|
||||
inv_2.save()
|
||||
inv_2.submit()
|
||||
|
||||
|
||||
@@ -79,8 +79,14 @@ frappe.require("assets/erpnext/js/financial_statements.js", function () {
|
||||
options: erpnext.get_presentation_currency_list(),
|
||||
},
|
||||
{
|
||||
fieldname: "with_period_closing_entry",
|
||||
label: __("Period Closing Entry"),
|
||||
fieldname: "with_period_closing_entry_for_opening",
|
||||
label: __("With Period Closing Entry For Opening Balances"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "with_period_closing_entry_for_current_period",
|
||||
label: __("Period Closing Entry For Current Period"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
|
||||
@@ -120,7 +120,7 @@ def get_data(filters):
|
||||
max_rgt,
|
||||
filters,
|
||||
gl_entries_by_account,
|
||||
ignore_closing_entries=not flt(filters.with_period_closing_entry),
|
||||
ignore_closing_entries=not flt(filters.with_period_closing_entry_for_current_period),
|
||||
ignore_opening_entries=True,
|
||||
)
|
||||
|
||||
@@ -274,7 +274,7 @@ def get_opening_balance(
|
||||
):
|
||||
opening_balance = opening_balance.where(closing_balance.posting_date >= filters.year_start_date)
|
||||
|
||||
if not flt(filters.with_period_closing_entry):
|
||||
if not flt(filters.with_period_closing_entry_for_opening):
|
||||
if doctype == "Account Closing Balance":
|
||||
opening_balance = opening_balance.where(closing_balance.is_period_closing_voucher_entry == 0)
|
||||
else:
|
||||
|
||||
@@ -4,15 +4,25 @@
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"mandatory": 1,
|
||||
"options": "Company",
|
||||
"wildcard_filter": 0
|
||||
}
|
||||
],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2019-01-17 17:20:42.374958",
|
||||
"modified": "2025-08-28 19:06:54.273322",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Trial Balance (Simple)",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"query": "select fiscal_year as \"Fiscal Year:Data:80\",\n\tcompany as \"Company:Data:220\",\n\tposting_date as \"Posting Date:Date:100\",\n\taccount as \"Account:Data:380\",\n\tsum(debit) as \"Debit:Currency:140\",\n\tsum(credit) as \"Credit:Currency:140\",\n\tfinance_book as \"Finance Book:Link/Finance Book:140\"\nfrom `tabGL Entry`\ngroup by fiscal_year, company, posting_date, account\norder by fiscal_year, company, posting_date, account",
|
||||
"query": "select fiscal_year as \"Fiscal Year:Data:80\",\n\tcompany as \"Company:Data:220\",\n\tposting_date as \"Posting Date:Date:100\",\n\taccount as \"Account:Data:380\",\n\tsum(debit) as \"Debit:Currency:140\",\n\tsum(credit) as \"Credit:Currency:140\",\n\tfinance_book as \"Finance Book:Link/Finance Book:140\"\nfrom `tabGL Entry`\nwhere is_cancelled = 0 and company = %(company)s\ngroup by fiscal_year, company, posting_date, account\norder by fiscal_year, company, posting_date, account",
|
||||
"ref_doctype": "GL Entry",
|
||||
"report_name": "Trial Balance (Simple)",
|
||||
"report_type": "Query Report",
|
||||
|
||||
@@ -86,7 +86,7 @@ def get_rate_as_at(date, from_currency, to_currency):
|
||||
return rate
|
||||
|
||||
|
||||
def convert_to_presentation_currency(gl_entries, currency_info):
|
||||
def convert_to_presentation_currency(gl_entries, currency_info, filters=None):
|
||||
"""
|
||||
Take a list of GL Entries and change the 'debit' and 'credit' values to currencies
|
||||
in `currency_info`.
|
||||
@@ -99,6 +99,13 @@ def convert_to_presentation_currency(gl_entries, currency_info):
|
||||
company_currency = currency_info["company_currency"]
|
||||
|
||||
account_currencies = list(set(entry["account_currency"] for entry in gl_entries))
|
||||
exchange_gain_or_loss = False
|
||||
|
||||
if filters and isinstance(filters.get("account"), list):
|
||||
account_filter = filters.get("account")
|
||||
gain_loss_account = frappe.db.get_value("Company", filters.company, "exchange_gain_loss_account")
|
||||
|
||||
exchange_gain_or_loss = len(account_filter) == 1 and account_filter[0] == gain_loss_account
|
||||
|
||||
for entry in gl_entries:
|
||||
debit = flt(entry["debit"])
|
||||
@@ -107,7 +114,11 @@ def convert_to_presentation_currency(gl_entries, currency_info):
|
||||
credit_in_account_currency = flt(entry["credit_in_account_currency"])
|
||||
account_currency = entry["account_currency"]
|
||||
|
||||
if len(account_currencies) == 1 and account_currency == presentation_currency:
|
||||
if (
|
||||
len(account_currencies) == 1
|
||||
and account_currency == presentation_currency
|
||||
and not exchange_gain_or_loss
|
||||
) and not (filters and filters.get("show_amount_in_company_currency")):
|
||||
entry["debit"] = debit_in_account_currency
|
||||
entry["credit"] = credit_in_account_currency
|
||||
else:
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
from collections import defaultdict
|
||||
from json import loads
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
from frappe import _, qb, throw
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import AliasedQuery, Criterion, Table
|
||||
from frappe.query_builder.functions import Round, Sum
|
||||
@@ -26,6 +28,7 @@ from frappe.utils import (
|
||||
nowdate,
|
||||
)
|
||||
from pypika import Order
|
||||
from pypika.functions import Coalesce
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
import erpnext
|
||||
@@ -2191,3 +2194,44 @@ def run_ledger_health_checks():
|
||||
doc.general_and_payment_ledger_mismatch = True
|
||||
doc.checked_on = run_date
|
||||
doc.save()
|
||||
|
||||
|
||||
def get_link_fields_grouped_by_option(doctype):
|
||||
meta = frappe.get_meta(doctype)
|
||||
link_fields_map = defaultdict(list)
|
||||
|
||||
for df in meta.fields:
|
||||
if df.fieldtype == "Link" and df.options and not df.ignore_user_permissions:
|
||||
link_fields_map[df.options].append(df.fieldname)
|
||||
|
||||
return link_fields_map
|
||||
|
||||
|
||||
def build_qb_match_conditions(doctype, user=None) -> list:
|
||||
match_filters = build_match_conditions(doctype, user, False)
|
||||
link_fields_map = get_link_fields_grouped_by_option(doctype)
|
||||
criterion = []
|
||||
apply_strict_user_permissions = frappe.get_system_settings("apply_strict_user_permissions")
|
||||
|
||||
if match_filters:
|
||||
_dt = qb.DocType(doctype)
|
||||
|
||||
for filter in match_filters:
|
||||
for link_option, allowed_values in filter.items():
|
||||
fieldnames = link_fields_map.get(link_option, [])
|
||||
cond = None
|
||||
|
||||
if link_option == doctype:
|
||||
cond = _dt["name"].isin(allowed_values)
|
||||
|
||||
for fieldname in fieldnames:
|
||||
field = _dt[fieldname]
|
||||
cond = field.isin(allowed_values)
|
||||
|
||||
if not apply_strict_user_permissions:
|
||||
cond = (Coalesce(field, "") == "") | cond
|
||||
|
||||
if cond:
|
||||
criterion.append(cond)
|
||||
|
||||
return criterion
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
"fieldname": "completion_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Completion Date",
|
||||
"mandatory_depends_on": "eval:doc.repair_status==\"Completed\"",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
@@ -264,7 +265,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-08-16 15:55:25.023471",
|
||||
"modified": "2025-07-29 15:14:34.044564",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Repair",
|
||||
@@ -302,10 +303,11 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "asset_name",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import add_months, cint, flt, getdate, time_diff_in_hours
|
||||
from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, time_diff_in_hours
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
@@ -20,6 +20,20 @@ class AssetRepair(AccountsController):
|
||||
self.set_stock_items_cost()
|
||||
self.calculate_total_repair_cost()
|
||||
|
||||
def validate_asset(self):
|
||||
if self.asset_doc.status in ("Sold", "Fully Depreciated", "Scrapped"):
|
||||
frappe.throw(
|
||||
_("Asset {0} is in {1} status and cannot be repaired.").format(
|
||||
get_link_to_form("Asset", self.asset), self.asset_doc.status
|
||||
)
|
||||
)
|
||||
|
||||
def validate_dates(self):
|
||||
if self.completion_date and (getdate(self.failure_date) > getdate(self.completion_date)):
|
||||
frappe.throw(
|
||||
_("Completion Date can not be before Failure Date. Please adjust the dates accordingly.")
|
||||
)
|
||||
|
||||
def update_status(self):
|
||||
if self.repair_status == "Pending":
|
||||
frappe.db.set_value("Asset", self.asset, "status", "Out of Order")
|
||||
@@ -195,7 +209,7 @@ class AssetRepair(AccountsController):
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": self.completion_date,
|
||||
"against_voucher_type": "Purchase Invoice",
|
||||
"against_voucher": self.purchase_invoice,
|
||||
"company": self.company,
|
||||
@@ -214,7 +228,7 @@ class AssetRepair(AccountsController):
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": self.completion_date,
|
||||
"company": self.company,
|
||||
},
|
||||
item=self,
|
||||
@@ -248,7 +262,7 @@ class AssetRepair(AccountsController):
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": self.completion_date,
|
||||
"company": self.company,
|
||||
},
|
||||
item=self,
|
||||
@@ -265,7 +279,7 @@ class AssetRepair(AccountsController):
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": self.completion_date,
|
||||
"against_voucher_type": "Stock Entry",
|
||||
"against_voucher": self.stock_entry,
|
||||
"company": self.company,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import flt, nowdate
|
||||
from frappe.utils import add_days, flt, nowdate
|
||||
|
||||
from erpnext.assets.doctype.asset.asset import (
|
||||
get_asset_account,
|
||||
@@ -288,6 +288,7 @@ def create_asset_repair(**args):
|
||||
|
||||
if args.submit:
|
||||
asset_repair.repair_status = "Completed"
|
||||
asset_repair.completion_date = add_days(args.failure_date, 1)
|
||||
asset_repair.cost_center = frappe.db.get_value("Company", asset.company, "cost_center")
|
||||
|
||||
if args.stock_consumption:
|
||||
|
||||
@@ -456,9 +456,8 @@ class PurchaseOrder(BuyingController):
|
||||
if not self.is_against_so():
|
||||
return
|
||||
for item in removed_items:
|
||||
prev_ordered_qty = (
|
||||
prev_ordered_qty = flt(
|
||||
frappe.get_cached_value("Sales Order Item", item.get("sales_order_item"), "ordered_qty")
|
||||
or 0.0
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -526,12 +526,8 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
self.assertRaises(frappe.ValidationError, pr.submit)
|
||||
self.assertRaises(frappe.ValidationError, pi.submit)
|
||||
|
||||
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
|
||||
def test_make_purchase_invoice_with_terms(self):
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import (
|
||||
automatically_fetch_payment_terms,
|
||||
)
|
||||
|
||||
automatically_fetch_payment_terms()
|
||||
po = create_purchase_order(do_not_save=True)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, make_pi_from_po, po.name)
|
||||
@@ -555,7 +551,6 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
self.assertEqual(getdate(pi.payment_schedule[0].due_date), getdate(po.transaction_date))
|
||||
self.assertEqual(pi.payment_schedule[1].payment_amount, 2500.0)
|
||||
self.assertEqual(getdate(pi.payment_schedule[1].due_date), add_days(getdate(po.transaction_date), 30))
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
|
||||
def test_warehouse_company_validation(self):
|
||||
from erpnext.stock.utils import InvalidWarehouseCompany
|
||||
@@ -703,6 +698,7 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
)
|
||||
self.assertEqual(due_date, "2023-03-31")
|
||||
|
||||
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 0})
|
||||
def test_terms_are_not_copied_if_automatically_fetch_payment_terms_is_unchecked(self):
|
||||
po = create_purchase_order(do_not_save=1)
|
||||
po.payment_terms_template = "_Test Payment Term Template"
|
||||
@@ -834,18 +830,16 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
bo.load_from_db()
|
||||
self.assertEqual(bo.items[0].ordered_qty, 5)
|
||||
|
||||
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
|
||||
def test_payment_terms_are_fetched_when_creating_purchase_invoice(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
|
||||
create_payment_terms_template,
|
||||
)
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import (
|
||||
automatically_fetch_payment_terms,
|
||||
compare_payment_schedules,
|
||||
)
|
||||
|
||||
automatically_fetch_payment_terms()
|
||||
|
||||
po = create_purchase_order(qty=10, rate=100, do_not_save=1)
|
||||
create_payment_terms_template()
|
||||
po.payment_terms_template = "Test Receivable Template"
|
||||
@@ -859,8 +853,6 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
# self.assertEqual(po.payment_terms_template, pi.payment_terms_template)
|
||||
compare_payment_schedules(self, po, pi)
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
|
||||
def test_internal_transfer_flow(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
make_inter_company_purchase_invoice,
|
||||
|
||||
@@ -9,6 +9,7 @@ from frappe import _
|
||||
from frappe.core.doctype.communication.email import make
|
||||
from frappe.desk.form.load import get_attachments
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder import Order
|
||||
from frappe.utils import get_url
|
||||
from frappe.utils.print_format import download_pdf
|
||||
from frappe.utils.user import get_user_fullname
|
||||
@@ -502,35 +503,32 @@ def get_supplier_tag():
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_rfq_containing_supplier(doctype, txt, searchfield, start, page_len, filters):
|
||||
conditions = ""
|
||||
if txt:
|
||||
conditions += "and rfq.name like '%%" + txt + "%%' "
|
||||
rfq = frappe.qb.DocType("Request for Quotation")
|
||||
rfq_supplier = frappe.qb.DocType("Request for Quotation Supplier")
|
||||
|
||||
if filters.get("transaction_date"):
|
||||
conditions += "and rfq.transaction_date = '{}'".format(filters.get("transaction_date"))
|
||||
|
||||
rfq_data = frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
distinct rfq.name, rfq.transaction_date,
|
||||
rfq.company
|
||||
from
|
||||
`tabRequest for Quotation` rfq, `tabRequest for Quotation Supplier` rfq_supplier
|
||||
where
|
||||
rfq.name = rfq_supplier.parent
|
||||
and rfq_supplier.supplier = %(supplier)s
|
||||
and rfq.docstatus = 1
|
||||
and rfq.company = %(company)s
|
||||
{conditions}
|
||||
order by rfq.transaction_date ASC
|
||||
limit %(page_len)s offset %(start)s """,
|
||||
{
|
||||
"page_len": page_len,
|
||||
"start": start,
|
||||
"company": filters.get("company"),
|
||||
"supplier": filters.get("supplier"),
|
||||
},
|
||||
as_dict=1,
|
||||
query = (
|
||||
frappe.qb.from_(rfq)
|
||||
.from_(rfq_supplier)
|
||||
.select(rfq.name)
|
||||
.distinct()
|
||||
.select(rfq.transaction_date, rfq.company)
|
||||
.where(
|
||||
(rfq.name == rfq_supplier.parent)
|
||||
& (rfq_supplier.supplier == filters.get("supplier"))
|
||||
& (rfq.docstatus == 1)
|
||||
& (rfq.company == filters.get("company"))
|
||||
)
|
||||
.orderby(rfq.transaction_date, order=Order.asc)
|
||||
.limit(page_len)
|
||||
.offset(start)
|
||||
)
|
||||
|
||||
if txt:
|
||||
query = query.where(rfq.name.like(f"%%{txt}%%"))
|
||||
|
||||
if filters.get("transaction_date"):
|
||||
query = query.where(rfq.transaction_date == filters.get("transaction_date"))
|
||||
|
||||
rfq_data = query.run(as_dict=1)
|
||||
|
||||
return rfq_data
|
||||
|
||||
@@ -42,6 +42,11 @@ frappe.ui.form.on("Supplier", {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.make_methods = {
|
||||
"Bank Account": () => erpnext.utils.make_bank_account(frm.doc.doctype, frm.doc.name),
|
||||
"Pricing Rule": () => erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name),
|
||||
};
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
@@ -84,21 +89,9 @@ frappe.ui.form.on("Supplier", {
|
||||
__("View")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Bank Account"),
|
||||
function () {
|
||||
erpnext.utils.make_bank_account(frm.doc.doctype, frm.doc.name);
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
frm.add_custom_button(__("Bank Account"), () => frm.make_methods["Bank Account"](), __("Create"));
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Pricing Rule"),
|
||||
function () {
|
||||
erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name);
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
frm.add_custom_button(__("Pricing Rule"), () => frm.make_methods["Pricing Rule"](), __("Create"));
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Get Supplier Group Details"),
|
||||
@@ -108,7 +101,10 @@ frappe.ui.form.on("Supplier", {
|
||||
__("Actions")
|
||||
);
|
||||
|
||||
if (cint(frappe.defaults.get_default("enable_common_party_accounting"))) {
|
||||
if (
|
||||
cint(frappe.defaults.get_default("enable_common_party_accounting")) &&
|
||||
frappe.model.can_create("Party Link")
|
||||
) {
|
||||
frm.add_custom_button(
|
||||
__("Link with Customer"),
|
||||
function () {
|
||||
|
||||
@@ -213,6 +213,11 @@ class AccountsController(TransactionBase):
|
||||
|
||||
self.validate_date_with_fiscal_year()
|
||||
self.validate_party_accounts()
|
||||
if self.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if self.is_return:
|
||||
self.validate_qty()
|
||||
else:
|
||||
self.validate_deferred_start_and_end_date()
|
||||
|
||||
self.validate_inter_company_reference()
|
||||
|
||||
@@ -258,11 +263,6 @@ class AccountsController(TransactionBase):
|
||||
|
||||
self.set_advance_gain_or_loss()
|
||||
|
||||
if self.is_return:
|
||||
self.validate_qty()
|
||||
else:
|
||||
self.validate_deferred_start_and_end_date()
|
||||
|
||||
self.validate_deferred_income_expense_account()
|
||||
self.set_inter_company_account()
|
||||
|
||||
@@ -1742,69 +1742,50 @@ class AccountsController(TransactionBase):
|
||||
def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on):
|
||||
from erpnext.controllers.status_updater import get_allowance_for
|
||||
|
||||
item_allowance = {}
|
||||
global_qty_allowance, global_amount_allowance = None, None
|
||||
ref_wise_billed_amount = self.get_reference_wise_billed_amt(ref_dt, item_ref_dn, based_on)
|
||||
|
||||
role_allowed_to_over_bill = frappe.get_cached_value(
|
||||
"Accounts Settings", None, "role_allowed_to_over_bill"
|
||||
)
|
||||
user_roles = frappe.get_roles()
|
||||
if not ref_wise_billed_amount:
|
||||
return
|
||||
|
||||
total_overbilled_amt = 0.0
|
||||
overbilled_items = []
|
||||
precision = self.precision(based_on, "items")
|
||||
precision_allowance = 1 / (10**precision)
|
||||
|
||||
reference_names = [d.get(item_ref_dn) for d in self.get("items") if d.get(item_ref_dn)]
|
||||
reference_details = self.get_billing_reference_details(reference_names, ref_dt + " Item", based_on)
|
||||
role_allowed_to_overbill = frappe.get_cached_value(
|
||||
"Accounts Settings", None, "role_allowed_to_over_bill"
|
||||
)
|
||||
is_overbilling_allowed = role_allowed_to_overbill in frappe.get_roles()
|
||||
|
||||
for item in self.get("items"):
|
||||
if not item.get(item_ref_dn):
|
||||
continue
|
||||
for row in ref_wise_billed_amount.values():
|
||||
total_billed_amt = row.billed_amt
|
||||
allowance = get_allowance_for(row.item_code, {}, None, None, "amount")[0]
|
||||
|
||||
ref_amt = flt(reference_details.get(item.get(item_ref_dn)), self.precision(based_on, item))
|
||||
based_on_amt = flt(item.get(based_on))
|
||||
|
||||
if not ref_amt:
|
||||
if based_on_amt: # Skip warning for free items
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"System will not check over billing since amount for Item {0} in {1} is zero"
|
||||
).format(item.item_code, ref_dt),
|
||||
title=_("Warning"),
|
||||
indicator="orange",
|
||||
)
|
||||
continue
|
||||
|
||||
already_billed = self.get_billed_amount_for_item(item, item_ref_dn, based_on)
|
||||
|
||||
total_billed_amt = flt(flt(already_billed) + based_on_amt, self.precision(based_on, item))
|
||||
|
||||
allowance, item_allowance, global_qty_allowance, global_amount_allowance = get_allowance_for(
|
||||
item.item_code, item_allowance, global_qty_allowance, global_amount_allowance, "amount"
|
||||
)
|
||||
|
||||
max_allowed_amt = flt(ref_amt * (100 + allowance) / 100)
|
||||
max_allowed_amt = flt(row.ref_amt * (100 + allowance) / 100)
|
||||
|
||||
if total_billed_amt < 0 and max_allowed_amt < 0:
|
||||
# while making debit note against purchase return entry(purchase receipt) getting overbill error
|
||||
total_billed_amt = abs(total_billed_amt)
|
||||
max_allowed_amt = abs(max_allowed_amt)
|
||||
total_billed_amt, max_allowed_amt = abs(total_billed_amt), abs(max_allowed_amt)
|
||||
|
||||
overbill_amt = total_billed_amt - max_allowed_amt
|
||||
row["max_allowed_amt"] = max_allowed_amt
|
||||
total_overbilled_amt += overbill_amt
|
||||
|
||||
if overbill_amt > 0.01 and role_allowed_to_over_bill not in user_roles:
|
||||
if self.doctype != "Purchase Invoice":
|
||||
self.throw_overbill_exception(item, max_allowed_amt)
|
||||
elif not cint(
|
||||
if overbill_amt > precision_allowance and not is_overbilling_allowed:
|
||||
if self.doctype != "Purchase Invoice" or not cint(
|
||||
frappe.db.get_single_value(
|
||||
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
|
||||
)
|
||||
):
|
||||
self.throw_overbill_exception(item, max_allowed_amt)
|
||||
overbilled_items.append(row)
|
||||
|
||||
if role_allowed_to_over_bill in user_roles and total_overbilled_amt > 0.1:
|
||||
if overbilled_items:
|
||||
self.throw_overbill_exception(overbilled_items, precision)
|
||||
|
||||
if is_overbilling_allowed and total_overbilled_amt > 0.1:
|
||||
frappe.msgprint(
|
||||
_("Overbilling of {} ignored because you have {} role.").format(
|
||||
total_overbilled_amt, role_allowed_to_over_bill
|
||||
total_overbilled_amt, role_allowed_to_overbill
|
||||
),
|
||||
indicator="orange",
|
||||
alert=True,
|
||||
@@ -1820,55 +1801,88 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
)
|
||||
|
||||
def get_billed_amount_for_item(self, item, item_ref_dn, based_on):
|
||||
def get_reference_wise_billed_amt(self, ref_dt, item_ref_dn, based_on):
|
||||
"""
|
||||
Returns Sum of Amount of
|
||||
Sales/Purchase Invoice Items
|
||||
that are linked to `item_ref_dn` (`dn_detail` / `pr_detail`)
|
||||
that are submitted OR not submitted but are under current invoice
|
||||
"""
|
||||
reference_names = [d.get(item_ref_dn) for d in self.items if d.get(item_ref_dn)]
|
||||
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Sum
|
||||
if not reference_names:
|
||||
return
|
||||
|
||||
item_doctype = frappe.qb.DocType(item.doctype)
|
||||
ref_wise_billed_amount = {}
|
||||
precision = self.precision(based_on, "items")
|
||||
reference_details = self.get_billing_reference_details(reference_names, ref_dt + " Item", based_on)
|
||||
already_billed = self.get_already_billed_amount(reference_names, item_ref_dn, based_on)
|
||||
|
||||
for item in self.items:
|
||||
key = item.get(item_ref_dn)
|
||||
if not key:
|
||||
continue
|
||||
|
||||
ref_amt = flt(reference_details.get(key), precision)
|
||||
current_amount = flt(item.get(based_on), precision)
|
||||
|
||||
if not ref_amt:
|
||||
if current_amount: # Skip warning for free items
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"System will not check over billing since amount for Item {0} in {1} is zero"
|
||||
).format(item.item_code, ref_dt),
|
||||
title=_("Warning"),
|
||||
indicator="orange",
|
||||
)
|
||||
continue
|
||||
|
||||
ref_wise_billed_amount.setdefault(
|
||||
key,
|
||||
frappe._dict(item_code=item.item_code, billed_amt=0.0, ref_amt=ref_amt, rows=[]),
|
||||
)
|
||||
|
||||
ref_wise_billed_amount[key]["rows"].append(item.idx)
|
||||
ref_wise_billed_amount[key]["ref_amt"] = ref_amt
|
||||
ref_wise_billed_amount[key]["billed_amt"] += current_amount
|
||||
if key in already_billed:
|
||||
ref_wise_billed_amount[key]["billed_amt"] += flt(already_billed.pop(key, 0), precision)
|
||||
|
||||
return ref_wise_billed_amount
|
||||
|
||||
def get_already_billed_amount(self, reference_names, item_ref_dn, based_on):
|
||||
item_doctype = frappe.qb.DocType(self.items[0].doctype)
|
||||
based_on_field = frappe.qb.Field(based_on)
|
||||
join_field = frappe.qb.Field(item_ref_dn)
|
||||
|
||||
result = (
|
||||
frappe.qb.from_(item_doctype)
|
||||
.select(Sum(based_on_field))
|
||||
.where(join_field == item.get(item_ref_dn))
|
||||
.where(
|
||||
Criterion.any(
|
||||
[ # select all items from other invoices OR current invoices
|
||||
Criterion.all(
|
||||
[ # for selecting items from other invoices
|
||||
item_doctype.docstatus == 1,
|
||||
item_doctype.parent != self.name,
|
||||
]
|
||||
),
|
||||
Criterion.all(
|
||||
[ # for selecting items from current invoice, that are linked to same reference
|
||||
item_doctype.docstatus == 0,
|
||||
item_doctype.parent == self.name,
|
||||
item_doctype.name != item.name,
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
).run()
|
||||
|
||||
return result[0][0] if result else 0
|
||||
|
||||
def throw_overbill_exception(self, item, max_allowed_amt):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings"
|
||||
).format(item.item_code, item.idx, max_allowed_amt)
|
||||
return frappe._dict(
|
||||
(
|
||||
frappe.qb.from_(item_doctype)
|
||||
.select(join_field, Sum(based_on_field))
|
||||
.where(join_field.isin(reference_names))
|
||||
.where((item_doctype.docstatus == 1) & (item_doctype.parent != self.name))
|
||||
.groupby(join_field)
|
||||
).run()
|
||||
)
|
||||
|
||||
def throw_overbill_exception(self, overbilled_items, precision):
|
||||
message = (
|
||||
_("<p>Cannot overbill for the following Items:</p>")
|
||||
+ "<ul>"
|
||||
+ "".join(
|
||||
_("<li>Item {0} in row(s) {1} billed more than {2}</li>").format(
|
||||
frappe.bold(item.item_code),
|
||||
", ".join(str(x) for x in item.rows),
|
||||
frappe.bold(fmt_money(item.max_allowed_amt, precision=precision, currency=self.currency)),
|
||||
)
|
||||
for item in overbilled_items
|
||||
)
|
||||
+ "</ul>"
|
||||
)
|
||||
message += _("<p>To allow over-billing, please set allowance in Accounts Settings.</p>")
|
||||
|
||||
frappe.throw(_(message))
|
||||
|
||||
def get_company_default(self, fieldname, ignore_validation=False):
|
||||
from erpnext.accounts.utils import get_company_default
|
||||
|
||||
@@ -2225,6 +2239,7 @@ class AccountsController(TransactionBase):
|
||||
|
||||
self.payment_schedule = []
|
||||
self.payment_terms_template = po_or_so.payment_terms_template
|
||||
posting_date = self.get("bill_date") or self.get("posting_date") or self.get("transaction_date")
|
||||
|
||||
for schedule in po_or_so.payment_schedule:
|
||||
payment_schedule = {
|
||||
@@ -2237,6 +2252,17 @@ class AccountsController(TransactionBase):
|
||||
}
|
||||
|
||||
if automatically_fetch_payment_terms:
|
||||
if schedule.due_date_based_on:
|
||||
payment_schedule["due_date"] = get_due_date(schedule, posting_date)
|
||||
payment_schedule["due_date_based_on"] = schedule.due_date_based_on
|
||||
payment_schedule["credit_days"] = cint(schedule.credit_days)
|
||||
payment_schedule["credit_months"] = cint(schedule.credit_months)
|
||||
|
||||
if schedule.discount_validity_based_on:
|
||||
payment_schedule["discount_date"] = get_discount_date(schedule, posting_date)
|
||||
payment_schedule["discount_validity_based_on"] = schedule.discount_validity_based_on
|
||||
payment_schedule["discount_validity"] = cint(schedule.discount_validity)
|
||||
|
||||
payment_schedule["payment_amount"] = flt(
|
||||
grand_total * flt(payment_schedule["invoice_portion"]) / 100,
|
||||
schedule.precision("payment_amount"),
|
||||
@@ -2724,9 +2750,7 @@ def set_balance_in_account_currency(
|
||||
_("Account: {0} with currency: {1} can not be selected").format(gl_dict.account, account_currency)
|
||||
)
|
||||
|
||||
gl_dict["account_currency"] = (
|
||||
company_currency if account_currency == company_currency else account_currency
|
||||
)
|
||||
gl_dict["account_currency"] = account_currency
|
||||
|
||||
# set debit/credit in account currency if not provided
|
||||
if flt(gl_dict.debit) and not flt(gl_dict.debit_in_account_currency):
|
||||
@@ -2973,14 +2997,26 @@ def get_payment_term_details(
|
||||
term = frappe.get_doc("Payment Term", term)
|
||||
else:
|
||||
term_details.payment_term = term.payment_term
|
||||
term_details.description = term.description
|
||||
term_details.invoice_portion = term.invoice_portion
|
||||
|
||||
fields_to_copy = [
|
||||
"description",
|
||||
"invoice_portion",
|
||||
"discount_type",
|
||||
"discount",
|
||||
"mode_of_payment",
|
||||
"due_date_based_on",
|
||||
"credit_days",
|
||||
"credit_months",
|
||||
"discount_validity_based_on",
|
||||
"discount_validity",
|
||||
]
|
||||
|
||||
for field in fields_to_copy:
|
||||
term_details[field] = term.get(field)
|
||||
|
||||
term_details.payment_amount = flt(term.invoice_portion) * flt(grand_total) / 100
|
||||
term_details.base_payment_amount = flt(term.invoice_portion) * flt(base_grand_total) / 100
|
||||
term_details.discount_type = term.discount_type
|
||||
term_details.discount = term.discount
|
||||
term_details.outstanding = term_details.payment_amount
|
||||
term_details.mode_of_payment = term.mode_of_payment
|
||||
|
||||
if bill_date:
|
||||
term_details.due_date = get_due_date(term, bill_date)
|
||||
@@ -2999,11 +3035,11 @@ def get_due_date(term, posting_date=None, bill_date=None):
|
||||
due_date = None
|
||||
date = bill_date or posting_date
|
||||
if term.due_date_based_on == "Day(s) after invoice date":
|
||||
due_date = add_days(date, term.credit_days)
|
||||
due_date = add_days(date, cint(term.credit_days))
|
||||
elif term.due_date_based_on == "Day(s) after the end of the invoice month":
|
||||
due_date = add_days(get_last_day(date), term.credit_days)
|
||||
due_date = add_days(get_last_day(date), cint(term.credit_days))
|
||||
elif term.due_date_based_on == "Month(s) after the end of the invoice month":
|
||||
due_date = get_last_day(add_months(date, term.credit_months))
|
||||
due_date = get_last_day(add_months(date, cint(term.credit_months)))
|
||||
return due_date
|
||||
|
||||
|
||||
@@ -3011,11 +3047,11 @@ def get_discount_date(term, posting_date=None, bill_date=None):
|
||||
discount_validity = None
|
||||
date = bill_date or posting_date
|
||||
if term.discount_validity_based_on == "Day(s) after invoice date":
|
||||
discount_validity = add_days(date, term.discount_validity)
|
||||
discount_validity = add_days(date, cint(term.discount_validity))
|
||||
elif term.discount_validity_based_on == "Day(s) after the end of the invoice month":
|
||||
discount_validity = add_days(get_last_day(date), term.discount_validity)
|
||||
discount_validity = add_days(get_last_day(date), cint(term.discount_validity))
|
||||
elif term.discount_validity_based_on == "Month(s) after the end of the invoice month":
|
||||
discount_validity = get_last_day(add_months(date, term.discount_validity))
|
||||
discount_validity = get_last_day(add_months(date, cint(term.discount_validity)))
|
||||
return discount_validity
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import json
|
||||
from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe import qb, scrub
|
||||
from frappe import cint, qb, scrub
|
||||
from frappe.desk.reportview import get_filters_cond, get_match_cond
|
||||
from frappe.query_builder import Criterion, CustomFunction
|
||||
from frappe.query_builder.functions import Locate
|
||||
@@ -528,21 +528,27 @@ def get_account_list(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters):
|
||||
return frappe.db.sql(
|
||||
"""select distinct bo.name, bo.blanket_order_type, bo.to_date
|
||||
from `tabBlanket Order` bo, `tabBlanket Order Item` boi
|
||||
where
|
||||
boi.parent = bo.name
|
||||
and boi.item_code = {item_code}
|
||||
and bo.blanket_order_type = '{blanket_order_type}'
|
||||
and bo.company = {company}
|
||||
and bo.docstatus = 1""".format(
|
||||
item_code=frappe.db.escape(filters.get("item")),
|
||||
blanket_order_type=filters.get("blanket_order_type"),
|
||||
company=frappe.db.escape(filters.get("company")),
|
||||
bo = frappe.qb.DocType("Blanket Order")
|
||||
bo_item = frappe.qb.DocType("Blanket Order Item")
|
||||
|
||||
blanket_orders = (
|
||||
frappe.qb.from_(bo)
|
||||
.from_(bo_item)
|
||||
.select(bo.name)
|
||||
.distinct()
|
||||
.select(bo.blanket_order_type, bo.to_date)
|
||||
.where(
|
||||
(bo_item.parent == bo.name)
|
||||
& (bo_item.item_code == filters.get("item"))
|
||||
& (bo.blanket_order_type == filters.get("blanket_order_type"))
|
||||
& (bo.company == filters.get("company"))
|
||||
& (bo.docstatus == 1)
|
||||
)
|
||||
.run()
|
||||
)
|
||||
|
||||
return blanket_orders
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
@@ -560,7 +566,7 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters):
|
||||
if filters.get("company"):
|
||||
condition += "and tabAccount.company = %(company)s"
|
||||
|
||||
condition += f"and tabAccount.disabled = {filters.get('disabled', 0)}"
|
||||
condition += " and tabAccount.disabled = %(disabled)s"
|
||||
|
||||
return frappe.db.sql(
|
||||
f"""select tabAccount.name from `tabAccount`
|
||||
@@ -570,7 +576,11 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters):
|
||||
and tabAccount.`{searchfield}` LIKE %(txt)s
|
||||
{condition} {get_match_cond(doctype)}
|
||||
order by idx desc, name""",
|
||||
{"txt": "%" + txt + "%", "company": filters.get("company", "")},
|
||||
{
|
||||
"txt": "%" + txt + "%",
|
||||
"company": filters.get("company", ""),
|
||||
"disabled": cint(filters.get("disabled", 0)),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -646,7 +656,7 @@ def get_expense_account(doctype, txt, searchfield, start, page_len, filters):
|
||||
where (tabAccount.report_type = "Profit and Loss"
|
||||
or tabAccount.account_type in ("Expense Account", "Fixed Asset", "Temporary", "Asset Received But Not Billed", "Capital Work in Progress"))
|
||||
and tabAccount.is_group=0
|
||||
and tabAccount.docstatus!=2
|
||||
and tabAccount.disabled = 0
|
||||
and tabAccount.{searchfield} LIKE %(txt)s
|
||||
{condition} {get_match_cond(doctype)}""",
|
||||
{"company": filters.get("company", ""), "txt": "%" + txt + "%"},
|
||||
|
||||
@@ -520,8 +520,9 @@ class StockController(AccountsController):
|
||||
|
||||
make_sl_entries(sl_entries, allow_negative_stock, via_landed_cost_voucher)
|
||||
|
||||
def make_gl_entries_on_cancel(self):
|
||||
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
||||
def make_gl_entries_on_cancel(self, from_repost=False):
|
||||
if not from_repost:
|
||||
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
||||
if frappe.db.sql(
|
||||
"""select name from `tabGL Entry` where voucher_type=%s
|
||||
and voucher_no=%s""",
|
||||
@@ -1027,12 +1028,7 @@ def is_reposting_pending():
|
||||
)
|
||||
|
||||
|
||||
def future_sle_exists(args, sl_entries=None, allow_force_reposting=True):
|
||||
if allow_force_reposting and frappe.db.get_single_value(
|
||||
"Stock Reposting Settings", "do_reposting_for_each_stock_transaction"
|
||||
):
|
||||
return True
|
||||
|
||||
def future_sle_exists(args, sl_entries=None):
|
||||
key = (args.voucher_type, args.voucher_no)
|
||||
if not hasattr(frappe.local, "future_sle"):
|
||||
frappe.local.future_sle = {}
|
||||
|
||||
@@ -60,7 +60,7 @@ def import_genericode():
|
||||
"doctype": "File",
|
||||
"attached_to_doctype": "Code List",
|
||||
"attached_to_name": code_list.name,
|
||||
"folder": "Home/Attachments",
|
||||
"folder": frappe.db.get_value("File", {"is_attachments_folder": 1}),
|
||||
"file_name": frappe.local.uploaded_filename,
|
||||
"file_url": frappe.local.uploaded_file_url,
|
||||
"is_private": 1,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"code_list",
|
||||
"canonical_uri",
|
||||
"title",
|
||||
"common_code",
|
||||
"description",
|
||||
@@ -71,10 +72,17 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Description",
|
||||
"max_height": "60px"
|
||||
},
|
||||
{
|
||||
"fetch_from": "code_list.canonical_uri",
|
||||
"fieldname": "canonical_uri",
|
||||
"fieldtype": "Data",
|
||||
"label": "Canonical URI"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"links": [],
|
||||
"modified": "2024-11-06 07:46:17.175687",
|
||||
"modified": "2025-10-04 17:22:28.176155",
|
||||
"modified_by": "Administrator",
|
||||
"module": "EDI",
|
||||
"name": "Common Code",
|
||||
@@ -94,10 +102,11 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "common_code,description",
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ class CommonCode(Document):
|
||||
|
||||
additional_data: DF.Code | None
|
||||
applies_to: DF.Table[DynamicLink]
|
||||
canonical_uri: DF.Data | None
|
||||
code_list: DF.Link
|
||||
common_code: DF.Data
|
||||
description: DF.SmallText | None
|
||||
|
||||
@@ -37,6 +37,14 @@ frappe.ui.form.on("Production Plan", {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("sub_assembly_warehouse", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("material_request", "material_requests", function () {
|
||||
return {
|
||||
filters: {
|
||||
|
||||
@@ -1048,6 +1048,7 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p
|
||||
item.purchase_uom,
|
||||
item_uom.conversion_factor,
|
||||
item.safety_stock,
|
||||
bom.item.as_("main_bom_item"),
|
||||
)
|
||||
.where(
|
||||
(bei.docstatus < 2)
|
||||
@@ -1115,6 +1116,7 @@ def get_subitems(
|
||||
item_default.default_warehouse,
|
||||
item.purchase_uom,
|
||||
item_uom.conversion_factor,
|
||||
bom.item.as_("main_bom_item"),
|
||||
)
|
||||
.where(
|
||||
(bom.name == bom_no)
|
||||
@@ -1228,6 +1230,7 @@ def get_material_request_items(
|
||||
"sales_order": sales_order,
|
||||
"description": row.get("description"),
|
||||
"uom": row.get("purchase_uom") or row.get("stock_uom"),
|
||||
"main_bom_item": row.get("main_bom_item"),
|
||||
}
|
||||
|
||||
|
||||
@@ -1757,6 +1760,7 @@ def get_raw_materials_of_sub_assembly_items(
|
||||
item.purchase_uom,
|
||||
item_uom.conversion_factor,
|
||||
item.safety_stock,
|
||||
bom.item.as_("main_bom_item"),
|
||||
)
|
||||
.where(
|
||||
(bei.docstatus == 1)
|
||||
|
||||
@@ -1322,20 +1322,20 @@ def stop_unstop(work_order, status):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def query_sales_order(production_item):
|
||||
out = frappe.db.sql_list(
|
||||
"""
|
||||
select distinct so.name from `tabSales Order` so, `tabSales Order Item` so_item
|
||||
where so_item.parent=so.name and so_item.item_code=%s and so.docstatus=1
|
||||
union
|
||||
select distinct so.name from `tabSales Order` so, `tabPacked Item` pi_item
|
||||
where pi_item.parent=so.name and pi_item.item_code=%s and so.docstatus=1
|
||||
""",
|
||||
(production_item, production_item),
|
||||
def query_sales_order(production_item: str) -> list[str]:
|
||||
return frappe.get_list(
|
||||
"Sales Order",
|
||||
filters=[
|
||||
["Sales Order", "docstatus", "=", 1],
|
||||
],
|
||||
or_filters=[
|
||||
["Sales Order Item", "item_code", "=", production_item],
|
||||
["Packed Item", "item_code", "=", production_item],
|
||||
],
|
||||
pluck="name",
|
||||
distinct=True,
|
||||
)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_job_card(work_order, operations):
|
||||
|
||||
@@ -79,7 +79,7 @@ def get_bom_stock(filters):
|
||||
BOM_ITEM.stock_qty,
|
||||
BOM_ITEM.stock_uom,
|
||||
BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity,
|
||||
Sum(BIN.actual_qty).as_("actual_qty"),
|
||||
BIN.actual_qty.as_("actual_qty"),
|
||||
Sum(Floor(BIN.actual_qty / (BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity))),
|
||||
)
|
||||
.where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM"))
|
||||
|
||||
@@ -292,12 +292,16 @@ def get_projectwise_timesheet_data(project=None, parent=None, from_time=None, to
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_timesheet_detail_rate(timelog, currency):
|
||||
timelog_detail = frappe.db.sql(
|
||||
f"""SELECT tsd.billing_amount as billing_amount,
|
||||
ts.currency as currency FROM `tabTimesheet Detail` tsd
|
||||
INNER JOIN `tabTimesheet` ts ON ts.name=tsd.parent
|
||||
WHERE tsd.name = '{timelog}'""",
|
||||
as_dict=1,
|
||||
ts = frappe.qb.DocType("Timesheet")
|
||||
ts_detail = frappe.qb.DocType("Timesheet Detail")
|
||||
|
||||
timelog_detail = (
|
||||
frappe.qb.from_(ts_detail)
|
||||
.inner_join(ts)
|
||||
.on(ts.name == ts_detail.parent)
|
||||
.select(ts_detail.billing_amount.as_("billing_amount"), ts.currency.as_("currency"))
|
||||
.where(ts_detail.name == timelog)
|
||||
.run(as_dict=1)
|
||||
)[0]
|
||||
|
||||
if timelog_detail.currency:
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import urllib.parse
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
def get_context(context):
|
||||
if frappe.form_dict.project:
|
||||
context.parents = [
|
||||
{"title": frappe.form_dict.project, "route": "/projects?project=" + frappe.form_dict.project}
|
||||
]
|
||||
context.success_url = "/projects?project=" + frappe.form_dict.project
|
||||
if project := frappe.form_dict.project:
|
||||
title = frappe.utils.data.escape_html(project)
|
||||
route = "/projects?" + urllib.parse.urlencode({"project": project})
|
||||
context.parents = [{"title": title, "route": route}]
|
||||
context.success_url = route
|
||||
|
||||
elif context.doc and context.doc.get("project"):
|
||||
context.parents = [
|
||||
{"title": context.doc.project, "route": "/projects?project=" + context.doc.project}
|
||||
]
|
||||
context.success_url = "/projects?project=" + context.doc.project
|
||||
elif context.doc and (project := context.doc.get("project")):
|
||||
title = frappe.utils.data.escape_html(project)
|
||||
route = "/projects?" + urllib.parse.urlencode({"project": project})
|
||||
context.parents = [{"title": title, "route": route}]
|
||||
context.success_url = route
|
||||
|
||||
@@ -76,9 +76,10 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
|
||||
// Update paid amount on return/debit note creation
|
||||
if (
|
||||
this.frm.doc.doctype === "Purchase Invoice"
|
||||
&& this.frm.doc.is_return
|
||||
&& (this.frm.doc.grand_total > this.frm.doc.paid_amount)
|
||||
this.frm.doc.doctype === "Purchase Invoice" &&
|
||||
this.frm.doc.is_return &&
|
||||
this.frm.doc.grand_total < 0 &&
|
||||
this.frm.doc.grand_total > this.frm.doc.paid_amount
|
||||
) {
|
||||
this.frm.doc.paid_amount = flt(this.frm.doc.grand_total, precision("grand_total"));
|
||||
}
|
||||
|
||||
@@ -878,6 +878,15 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
}
|
||||
|
||||
discount_date(doc, cdt, cdn) {
|
||||
// Remove fields as discount_date is auto-managed by payment terms
|
||||
const row = locals[cdt][cdn];
|
||||
["discount_validity", "discount_validity_based_on"].forEach((field) => {
|
||||
row[field] = "";
|
||||
});
|
||||
this.frm.refresh_field("payment_schedule");
|
||||
}
|
||||
|
||||
due_date() {
|
||||
// due_date is to be changed, payment terms template and/or payment schedule must
|
||||
// be removed as due_date is automatically changed based on payment terms
|
||||
@@ -2203,6 +2212,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
'item_code': item.item_code,
|
||||
'valid_from': ["<=", doc.transaction_date || doc.bill_date || doc.posting_date],
|
||||
'item_group': item.item_group,
|
||||
'disabled': 0,
|
||||
}
|
||||
|
||||
if (doc.tax_category)
|
||||
@@ -2244,7 +2254,18 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
payment_term(doc, cdt, cdn) {
|
||||
const me = this;
|
||||
var row = locals[cdt][cdn];
|
||||
if(row.payment_term) {
|
||||
// empty date condition fields
|
||||
[
|
||||
"due_date_based_on",
|
||||
"credit_days",
|
||||
"credit_months",
|
||||
"discount_validity",
|
||||
"discount_validity_based_on",
|
||||
].forEach(function (field) {
|
||||
row[field] = "";
|
||||
});
|
||||
|
||||
if (row.payment_term) {
|
||||
frappe.call({
|
||||
method: "erpnext.controllers.accounts_controller.get_payment_term_details",
|
||||
args: {
|
||||
@@ -2254,16 +2275,19 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
grand_total: this.frm.doc.rounded_total || this.frm.doc.grand_total,
|
||||
base_grand_total: this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total
|
||||
},
|
||||
callback: function(r) {
|
||||
if(r.message && !r.exc) {
|
||||
for (var d in r.message) {
|
||||
frappe.model.set_value(cdt, cdn, d, r.message[d]);
|
||||
const company_currency = me.get_company_currency();
|
||||
me.update_payment_schedule_grid_labels(company_currency);
|
||||
callback: function (r) {
|
||||
if (r.message && !r.exc) {
|
||||
const company_currency = me.get_company_currency();
|
||||
for (let d in r.message) {
|
||||
row[d] = r.message[d];
|
||||
}
|
||||
me.update_payment_schedule_grid_labels(company_currency);
|
||||
me.frm.refresh_field("payment_schedule");
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
} else {
|
||||
me.frm.refresh_field("payment_schedule");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -215,17 +215,9 @@ $.extend(erpnext.utils, {
|
||||
},
|
||||
|
||||
make_bank_account: function (doctype, docname) {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.bank_account.bank_account.make_bank_account",
|
||||
args: {
|
||||
doctype: doctype,
|
||||
docname: docname,
|
||||
},
|
||||
freeze: true,
|
||||
callback: function (r) {
|
||||
var doclist = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
|
||||
},
|
||||
frappe.new_doc("Bank Account", {
|
||||
party_type: doctype,
|
||||
party: docname,
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ frappe.ui.form.on("Customer", {
|
||||
method: "erpnext.selling.doctype.customer.customer.make_opportunity",
|
||||
frm: cur_frm,
|
||||
}),
|
||||
"Bank Account": () => erpnext.utils.make_bank_account(frm.doc.doctype, frm.doc.name),
|
||||
};
|
||||
|
||||
frm.add_fetch("lead_name", "company_name", "customer_name");
|
||||
@@ -155,7 +156,10 @@ frappe.ui.form.on("Customer", {
|
||||
__("Actions")
|
||||
);
|
||||
|
||||
if (cint(frappe.defaults.get_default("enable_common_party_accounting"))) {
|
||||
if (
|
||||
cint(frappe.defaults.get_default("enable_common_party_accounting")) &&
|
||||
frappe.model.can_create("Party Link")
|
||||
) {
|
||||
frm.add_custom_button(
|
||||
__("Link with Supplier"),
|
||||
function () {
|
||||
|
||||
@@ -1266,6 +1266,11 @@ def make_raw_material_request(items, company, sales_order, project=None):
|
||||
|
||||
items.update({"company": company, "sales_order": sales_order})
|
||||
|
||||
item_wh = {}
|
||||
for item in items.get("items"):
|
||||
if item.get("warehouse"):
|
||||
item_wh[item.get("item_code")] = item.get("warehouse")
|
||||
|
||||
raw_materials = get_items_for_material_requests(items)
|
||||
if not raw_materials:
|
||||
frappe.msgprint(_("Material Request not created, as quantity for Raw Materials already available."))
|
||||
@@ -1290,7 +1295,7 @@ def make_raw_material_request(items, company, sales_order, project=None):
|
||||
"item_code": item.get("item_code"),
|
||||
"qty": item.get("quantity"),
|
||||
"schedule_date": schedule_date,
|
||||
"warehouse": item.get("warehouse"),
|
||||
"warehouse": item_wh.get(item.get("main_bom_item")) or item.get("warehouse"),
|
||||
"sales_order": sales_order,
|
||||
"project": project,
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@ from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, getdate, nowdate, today
|
||||
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.controllers.accounts_controller import update_child_qty_rate
|
||||
from erpnext.controllers.accounts_controller import get_due_date, update_child_qty_rate
|
||||
from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import (
|
||||
make_maintenance_schedule,
|
||||
)
|
||||
@@ -1759,14 +1759,13 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
so.load_from_db()
|
||||
self.assertRaises(frappe.LinkExistsError, so.cancel)
|
||||
|
||||
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
|
||||
def test_payment_terms_are_fetched_when_creating_sales_invoice(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
|
||||
create_payment_terms_template,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
automatically_fetch_payment_terms()
|
||||
|
||||
so = make_sales_order(uom="Nos", do_not_save=1)
|
||||
create_payment_terms_template()
|
||||
so.payment_terms_template = "Test Receivable Template"
|
||||
@@ -1780,8 +1779,6 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertEqual(so.payment_terms_template, si.payment_terms_template)
|
||||
compare_payment_schedules(self, so, si)
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
|
||||
def test_zero_amount_sales_order_billing_status(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
@@ -2284,16 +2281,14 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertRaises(frappe.ValidationError, so1.update_status, "Draft")
|
||||
|
||||
|
||||
def automatically_fetch_payment_terms(enable=1):
|
||||
accounts_settings = frappe.get_doc("Accounts Settings")
|
||||
accounts_settings.automatically_fetch_payment_terms = enable
|
||||
accounts_settings.save()
|
||||
|
||||
|
||||
def compare_payment_schedules(doc, doc1, doc2):
|
||||
for index, schedule in enumerate(doc1.get("payment_schedule")):
|
||||
posting_date = doc1.get("bill_date") or doc1.get("posting_date") or doc1.get("transaction_date")
|
||||
due_date = schedule.due_date
|
||||
if schedule.due_date_based_on:
|
||||
due_date = get_due_date(schedule, posting_date=posting_date)
|
||||
doc.assertEqual(schedule.payment_term, doc2.payment_schedule[index].payment_term)
|
||||
doc.assertEqual(getdate(schedule.due_date), doc2.payment_schedule[index].due_date)
|
||||
doc.assertEqual(due_date, doc2.payment_schedule[index].due_date)
|
||||
doc.assertEqual(schedule.invoice_portion, doc2.payment_schedule[index].invoice_portion)
|
||||
doc.assertEqual(schedule.payment_amount, doc2.payment_schedule[index].payment_amount)
|
||||
|
||||
|
||||
@@ -37,15 +37,15 @@ class SellingSettings(Document):
|
||||
)
|
||||
|
||||
def toggle_hide_tax_id(self):
|
||||
self.hide_tax_id = cint(self.hide_tax_id)
|
||||
_hide_tax_id = cint(self.hide_tax_id)
|
||||
|
||||
# Make property setters to hide tax_id fields
|
||||
for doctype in ("Sales Order", "Sales Invoice", "Delivery Note"):
|
||||
make_property_setter(
|
||||
doctype, "tax_id", "hidden", self.hide_tax_id, "Check", validate_fields_for_doctype=False
|
||||
doctype, "tax_id", "hidden", _hide_tax_id, "Check", validate_fields_for_doctype=False
|
||||
)
|
||||
make_property_setter(
|
||||
doctype, "tax_id", "print_hide", self.hide_tax_id, "Check", validate_fields_for_doctype=False
|
||||
doctype, "tax_id", "print_hide", _hide_tax_id, "Check", validate_fields_for_doctype=False
|
||||
)
|
||||
|
||||
def toggle_editable_rate_for_bundle_items(self):
|
||||
|
||||
@@ -21,6 +21,12 @@ erpnext.setup.EmployeeController = class EmployeeController extends frappe.ui.fo
|
||||
};
|
||||
|
||||
frappe.ui.form.on("Employee", {
|
||||
setup: function (frm) {
|
||||
frm.make_methods = {
|
||||
"Bank Account": () => erpnext.utils.make_bank_account(frm.doc.doctype, frm.doc.name),
|
||||
};
|
||||
},
|
||||
|
||||
onload: function (frm) {
|
||||
frm.set_query("department", function () {
|
||||
return {
|
||||
|
||||
@@ -2,8 +2,10 @@ frappe.listview_settings["Employee"] = {
|
||||
add_fields: ["status", "branch", "department", "designation", "image"],
|
||||
filters: [["status", "=", "Active"]],
|
||||
get_indicator: function (doc) {
|
||||
var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status];
|
||||
indicator[1] = { Active: "green", Inactive: "red", Left: "gray", Suspended: "orange" }[doc.status];
|
||||
return indicator;
|
||||
return [
|
||||
__(doc.status, null, "Employee"),
|
||||
{ Active: "green", Inactive: "red", Left: "gray", Suspended: "orange" }[doc.status],
|
||||
"status,=," + doc.status,
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,8 +5,10 @@ import unittest
|
||||
|
||||
import frappe
|
||||
import frappe.utils
|
||||
from frappe.query_builder import Criterion
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.utils import build_qb_match_conditions
|
||||
from erpnext.setup.doctype.employee.employee import InactiveEmployeeStatusError
|
||||
|
||||
test_records = frappe.get_test_records("Employee")
|
||||
@@ -34,6 +36,32 @@ class TestEmployee(unittest.TestCase):
|
||||
employee_doc.save()
|
||||
self.assertTrue("Employee" not in frappe.get_roles(user))
|
||||
|
||||
def test_employee_user_permission(self):
|
||||
employee1 = make_employee("employee_1_test@company.com", create_user_permission=1)
|
||||
employee2 = make_employee("employee_2_test@company.com", create_user_permission=1)
|
||||
make_employee("employee_3_test@company.com", create_user_permission=1)
|
||||
|
||||
employee1_doc = frappe.get_doc("Employee", employee1)
|
||||
employee2_doc = frappe.get_doc("Employee", employee2)
|
||||
|
||||
employee2_doc.reload()
|
||||
employee2_doc.reports_to = employee1_doc.name
|
||||
employee2_doc.save()
|
||||
|
||||
frappe.set_user(employee1_doc.user_id)
|
||||
|
||||
Employee = frappe.qb.DocType("Employee")
|
||||
qb_employee_list = (
|
||||
frappe.qb.from_(Employee)
|
||||
.select(Employee.name)
|
||||
.where(Criterion.all(build_qb_match_conditions("Employee")))
|
||||
.orderby(Employee.Name)
|
||||
).run(pluck=Employee.name)
|
||||
employee_list = frappe.db.get_list("Employee", pluck="name", order_by="name")
|
||||
|
||||
self.assertEqual(qb_employee_list, employee_list)
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ def update_qty(bin_name, args):
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
|
||||
# actual qty is not up to date in case of backdated transaction
|
||||
if future_sle_exists(args, allow_force_reposting=False):
|
||||
if future_sle_exists(args):
|
||||
last_sle_qty = (
|
||||
frappe.qb.from_(sle)
|
||||
.select(sle.qty_after_transaction)
|
||||
|
||||
@@ -13,7 +13,6 @@ from erpnext.accounts.utils import get_balance_on
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import (
|
||||
automatically_fetch_payment_terms,
|
||||
compare_payment_schedules,
|
||||
create_dn_against_so,
|
||||
make_sales_order,
|
||||
@@ -1026,14 +1025,13 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
|
||||
self.assertTrue("TESTBATCH" in dn.packed_items[0].batch_no, "Batch number not added in packed item")
|
||||
|
||||
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
|
||||
def test_payment_terms_are_fetched_when_creating_sales_invoice(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
|
||||
create_payment_terms_template,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
automatically_fetch_payment_terms()
|
||||
|
||||
so = make_sales_order(uom="Nos", do_not_save=1)
|
||||
create_payment_terms_template()
|
||||
so.payment_terms_template = "Test Receivable Template"
|
||||
@@ -1053,8 +1051,6 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
self.assertEqual(so.payment_terms_template, si.payment_terms_template)
|
||||
compare_payment_schedules(self, so, si)
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
|
||||
def test_returned_qty_in_return_dn(self):
|
||||
# SO ---> SI ---> DN
|
||||
# |
|
||||
|
||||
@@ -75,7 +75,9 @@ frappe.ui.form.on("Inventory Dimension", {
|
||||
|
||||
set_parent_fields(frm) {
|
||||
if (frm.doc.apply_to_all_doctypes) {
|
||||
frm.set_df_property("fetch_from_parent", "options", frm.doc.reference_document);
|
||||
let options = ["\n", frm.doc.reference_document];
|
||||
|
||||
frm.set_df_property("fetch_from_parent", "options", options);
|
||||
} else if (frm.doc.document_type && frm.doc.istable) {
|
||||
frappe.call({
|
||||
method: "erpnext.stock.doctype.inventory_dimension.inventory_dimension.get_parent_fields",
|
||||
@@ -85,7 +87,7 @@ frappe.ui.form.on("Inventory Dimension", {
|
||||
},
|
||||
callback: (r) => {
|
||||
if (r.message && r.message.length) {
|
||||
frm.set_df_property("fetch_from_parent", "options", [""].concat(r.message));
|
||||
frm.set_df_property("fetch_from_parent", "options", ["\n"].concat(r.message));
|
||||
} else {
|
||||
frm.set_df_property("fetch_from_parent", "hidden", 1);
|
||||
}
|
||||
|
||||
@@ -143,7 +143,6 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.apply_to_all_doctypes",
|
||||
"description": "Set fieldname from which you want to fetch the data from the parent form.",
|
||||
"fieldname": "fetch_from_parent",
|
||||
"fieldtype": "Select",
|
||||
@@ -189,7 +188,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-10-05 12:52:18.705431",
|
||||
"modified": "2025-07-07 15:51:29.329064",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Inventory Dimension",
|
||||
@@ -236,4 +235,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,16 +40,11 @@ class InventoryDimension(Document):
|
||||
self.reset_value()
|
||||
self.set_source_and_target_fieldname()
|
||||
self.set_type_of_transaction()
|
||||
self.set_fetch_value_from()
|
||||
|
||||
def set_type_of_transaction(self):
|
||||
if self.apply_to_all_doctypes:
|
||||
self.type_of_transaction = "Both"
|
||||
|
||||
def set_fetch_value_from(self):
|
||||
if self.apply_to_all_doctypes:
|
||||
self.fetch_from_parent = self.reference_document
|
||||
|
||||
def do_not_update_document(self):
|
||||
if self.is_new() or not self.has_stock_ledger():
|
||||
return
|
||||
|
||||
@@ -154,6 +154,8 @@ class TestInventoryDimension(FrappeTestCase):
|
||||
reference_document="Rack", dimension_name="Rack", apply_to_all_doctypes=1
|
||||
)
|
||||
|
||||
inv_dimension.db_set("fetch_from_parent", "Rack")
|
||||
|
||||
self.assertEqual(inv_dimension.type_of_transaction, "Both")
|
||||
self.assertEqual(inv_dimension.fetch_from_parent, "Rack")
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import json
|
||||
import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder import Order
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate
|
||||
|
||||
@@ -498,39 +499,44 @@ def get_items_based_on_default_supplier(supplier):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, page_len, filters):
|
||||
conditions = ""
|
||||
if txt:
|
||||
conditions += "and mr.name like '%%" + txt + "%%' "
|
||||
|
||||
if filters.get("transaction_date"):
|
||||
date = filters.get("transaction_date")[1]
|
||||
conditions += f"and mr.transaction_date between '{date[0]}' and '{date[1]}' "
|
||||
|
||||
supplier = filters.get("supplier")
|
||||
supplier_items = get_items_based_on_default_supplier(supplier)
|
||||
|
||||
if not supplier_items:
|
||||
frappe.throw(_("{0} is not the default supplier for any items.").format(supplier))
|
||||
|
||||
material_requests = frappe.db.sql(
|
||||
"""select distinct mr.name, transaction_date,company
|
||||
from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item
|
||||
where mr.name = mr_item.parent
|
||||
and mr_item.item_code in ({})
|
||||
and mr.material_request_type = 'Purchase'
|
||||
and mr.per_ordered < 99.99
|
||||
and mr.docstatus = 1
|
||||
and mr.status != 'Stopped'
|
||||
and mr.company = %s
|
||||
{}
|
||||
order by mr_item.item_code ASC
|
||||
limit {} offset {} """.format(
|
||||
", ".join(["%s"] * len(supplier_items)), conditions, cint(page_len), cint(start)
|
||||
),
|
||||
(*tuple(supplier_items), filters.get("company")),
|
||||
as_dict=1,
|
||||
mr = frappe.qb.DocType("Material Request")
|
||||
mr_item = frappe.qb.DocType("Material Request Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(mr)
|
||||
.from_(mr_item)
|
||||
.select(mr.name)
|
||||
.distinct()
|
||||
.select(mr.transaction_date, mr.company)
|
||||
.where(
|
||||
(mr.name == mr_item.parent)
|
||||
& (mr_item.item_code.isin(supplier_items))
|
||||
& (mr.material_request_type == "Purchase")
|
||||
& (mr.per_ordered < 99.99)
|
||||
& (mr.docstatus == 1)
|
||||
& (mr.status != "Stopped")
|
||||
& (mr.company == filters.get("company"))
|
||||
)
|
||||
.orderby(mr_item.item_code, order=Order.asc)
|
||||
.limit(cint(page_len))
|
||||
.offset(cint(start))
|
||||
)
|
||||
|
||||
if txt:
|
||||
query = query.where(mr.name.like(f"%%{txt}%%"))
|
||||
|
||||
if filters.get("transaction_date"):
|
||||
date = filters.get("transaction_date")[1]
|
||||
query = query.where(mr.transaction_date[date[0] : date[1]])
|
||||
|
||||
material_requests = query.run(as_dict=True)
|
||||
|
||||
return material_requests
|
||||
|
||||
|
||||
|
||||
@@ -1077,7 +1077,8 @@
|
||||
"no_copy": 1,
|
||||
"options": "Delivery Note",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "scan_barcode",
|
||||
@@ -1242,7 +1243,7 @@
|
||||
"idx": 261,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-13 16:55:14.129055",
|
||||
"modified": "2025-08-06 16:41:02.690658",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt",
|
||||
|
||||
@@ -1056,6 +1056,7 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
|
||||
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
|
||||
|
||||
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
|
||||
def test_payment_terms_are_fetched_when_creating_purchase_invoice(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
|
||||
create_payment_terms_template,
|
||||
@@ -1066,12 +1067,9 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
make_pr_against_po,
|
||||
)
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import (
|
||||
automatically_fetch_payment_terms,
|
||||
compare_payment_schedules,
|
||||
)
|
||||
|
||||
automatically_fetch_payment_terms()
|
||||
|
||||
po = create_purchase_order(qty=10, rate=100, do_not_save=1)
|
||||
create_payment_terms_template()
|
||||
po.payment_terms_template = "Test Receivable Template"
|
||||
@@ -1089,8 +1087,6 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
# self.assertEqual(po.payment_terms_template, pi.payment_terms_template)
|
||||
compare_payment_schedules(self, po, pi)
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
|
||||
@change_settings("Stock Settings", {"allow_negative_stock": 1})
|
||||
def test_neg_to_positive(self):
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
@@ -2761,6 +2757,63 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
pr.reload()
|
||||
self.assertEqual(pr.status, "To Bill")
|
||||
|
||||
def test_serial_no_exists_in_future(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
item_doc = make_item(
|
||||
"Test Serial No Item Exists in Future",
|
||||
{
|
||||
"is_purchase_item": 1,
|
||||
"is_stock_item": 1,
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "SN-SBNS-.#####",
|
||||
},
|
||||
)
|
||||
|
||||
source_warehouse = "_Test Warehouse - _TC"
|
||||
target_warehouse = "_Test Warehouse 1 - _TC"
|
||||
if not frappe.db.exists("Warehouse", target_warehouse):
|
||||
create_warehouse("_Test Warehouse 1")
|
||||
|
||||
make_purchase_receipt(
|
||||
item_code=item_doc.name,
|
||||
qty=1,
|
||||
rate=100,
|
||||
serial_no="SN-SBNS-00001",
|
||||
posting_date=add_days(today(), -2),
|
||||
)
|
||||
|
||||
make_stock_entry(
|
||||
item_code=item_doc.name,
|
||||
qty=1,
|
||||
rate=100,
|
||||
to_warehouse=target_warehouse,
|
||||
serial_no="SN-SBNS-00002",
|
||||
posting_date=add_days(today(), -1),
|
||||
)
|
||||
|
||||
make_stock_entry(
|
||||
item_code=item_doc.name,
|
||||
qty=1,
|
||||
rate=100,
|
||||
from_warehouse=source_warehouse,
|
||||
to_warehouse=target_warehouse,
|
||||
serial_no="SN-SBNS-00001",
|
||||
posting_date=today(),
|
||||
)
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=item_doc.name,
|
||||
qty=1,
|
||||
rate=100,
|
||||
from_warehouse=target_warehouse,
|
||||
serial_no="SN-SBNS-00001",
|
||||
posting_date=add_days(today(), -1),
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, se.submit)
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
|
||||
@@ -8,7 +8,18 @@ import frappe
|
||||
from frappe import ValidationError, _
|
||||
from frappe.model.naming import make_autoname
|
||||
from frappe.query_builder.functions import Coalesce
|
||||
from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, now, nowdate, safe_json_loads
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
cint,
|
||||
cstr,
|
||||
flt,
|
||||
get_datetime,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
now,
|
||||
nowdate,
|
||||
safe_json_loads,
|
||||
)
|
||||
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.stock.get_item_details import get_reserved_qty_for_so
|
||||
@@ -197,7 +208,7 @@ class SerialNo(StockController):
|
||||
for sle in frappe.db.sql(
|
||||
"""
|
||||
SELECT voucher_type, voucher_no,
|
||||
posting_date, posting_time, incoming_rate, actual_qty, serial_no
|
||||
posting_date, posting_time, incoming_rate, actual_qty, serial_no, posting_datetime
|
||||
FROM
|
||||
`tabStock Ledger Entry`
|
||||
WHERE
|
||||
@@ -251,8 +262,23 @@ class SerialNo(StockController):
|
||||
_("Cannot delete Serial No {0}, as it is used in stock transactions").format(self.name)
|
||||
)
|
||||
|
||||
def update_serial_no_reference(self, serial_no=None):
|
||||
def update_serial_no_reference(self, serial_no=None, sle=None):
|
||||
last_sle = self.get_last_sle(serial_no)
|
||||
|
||||
_last_sle_dict = last_sle.get("last_sle")
|
||||
if (
|
||||
_last_sle_dict
|
||||
and sle.get("voucher_type") != "Stock Reconciliation"
|
||||
and sle.get("voucher_no") != _last_sle_dict.get("voucher_no")
|
||||
and get_datetime(sle.get("posting_datetime"))
|
||||
< get_datetime(_last_sle_dict.get("posting_datetime"))
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"You can not complete this transaction because a future transaction exists for the serial number {0}"
|
||||
).format(serial_no)
|
||||
)
|
||||
|
||||
self.set_purchase_details(last_sle.get("purchase_sle"))
|
||||
self.set_sales_details(last_sle.get("delivery_sle"))
|
||||
self.set_maintenance_status()
|
||||
@@ -770,7 +796,7 @@ def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False):
|
||||
serial_no_doc.sales_order = None
|
||||
|
||||
serial_no_doc.validate_item()
|
||||
serial_no_doc.update_serial_no_reference(serial_no)
|
||||
serial_no_doc.update_serial_no_reference(serial_no, sle=args)
|
||||
|
||||
if is_new:
|
||||
serial_no_doc.db_insert()
|
||||
|
||||
@@ -748,12 +748,12 @@ def get_items(warehouse, posting_date, posting_time, company, item_code=None, ig
|
||||
itemwise_batch_data = get_itemwise_batch(warehouse, posting_date, company, item_code)
|
||||
|
||||
for d in items:
|
||||
if d.item_code in itemwise_batch_data:
|
||||
if (d.item_code, d.warehouse) in itemwise_batch_data:
|
||||
valuation_rate = get_stock_balance(
|
||||
d.item_code, d.warehouse, posting_date, posting_time, with_valuation_rate=True
|
||||
)[1]
|
||||
|
||||
for row in itemwise_batch_data.get(d.item_code):
|
||||
for row in itemwise_batch_data.get((d.item_code, d.warehouse)):
|
||||
if ignore_empty_stock and not row.qty:
|
||||
continue
|
||||
|
||||
@@ -885,7 +885,7 @@ def get_itemwise_batch(warehouse, posting_date, company, item_code=None):
|
||||
columns, data = execute(filters)
|
||||
|
||||
for row in data:
|
||||
itemwise_batch_data.setdefault(row[0], []).append(
|
||||
itemwise_batch_data.setdefault((row[0], row[3]), []).append(
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": row[0],
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"end_time",
|
||||
"limits_dont_apply_on",
|
||||
"item_based_reposting",
|
||||
"do_reposting_for_each_stock_transaction",
|
||||
"errors_notification_section",
|
||||
"notify_reposting_error_to_role"
|
||||
],
|
||||
@@ -66,18 +65,12 @@
|
||||
"fieldname": "errors_notification_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Errors Notification"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "do_reposting_for_each_stock_transaction",
|
||||
"fieldtype": "Check",
|
||||
"label": "Do reposting for each Stock Transaction"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-04-24 12:19:40.204888",
|
||||
"modified": "2025-07-08 11:27:46.659056",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Reposting Settings",
|
||||
|
||||
@@ -11,10 +11,6 @@ class StockRepostingSettings(Document):
|
||||
def validate(self):
|
||||
self.set_minimum_reposting_time_slot()
|
||||
|
||||
def before_save(self):
|
||||
if self.do_reposting_for_each_stock_transaction:
|
||||
self.item_based_reposting = 1
|
||||
|
||||
def set_minimum_reposting_time_slot(self):
|
||||
"""Ensure that timeslot for reposting is at least 12 hours."""
|
||||
if not self.limit_reposting_timeslot:
|
||||
|
||||
@@ -38,51 +38,3 @@ class TestStockRepostingSettings(unittest.TestCase):
|
||||
|
||||
users = get_recipients()
|
||||
self.assertTrue(user in users)
|
||||
|
||||
def test_do_reposting_for_each_stock_transaction(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 1)
|
||||
if frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"):
|
||||
frappe.db.set_single_value("Stock Reposting Settings", "item_based_reposting", 0)
|
||||
|
||||
item = make_item(
|
||||
"_Test item for reposting check for each transaction", properties={"is_stock_item": 1}
|
||||
).name
|
||||
|
||||
stock_entry = make_stock_entry(
|
||||
item_code=item,
|
||||
qty=1,
|
||||
rate=100,
|
||||
stock_entry_type="Material Receipt",
|
||||
target="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
riv = frappe.get_all("Repost Item Valuation", filters={"voucher_no": stock_entry.name}, pluck="name")
|
||||
self.assertTrue(riv)
|
||||
|
||||
frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 0)
|
||||
|
||||
def test_do_not_reposting_for_each_stock_transaction(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 0)
|
||||
if frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"):
|
||||
frappe.db.set_single_value("Stock Reposting Settings", "item_based_reposting", 0)
|
||||
|
||||
item = make_item(
|
||||
"_Test item for do not reposting check for each transaction", properties={"is_stock_item": 1}
|
||||
).name
|
||||
|
||||
stock_entry = make_stock_entry(
|
||||
item_code=item,
|
||||
qty=1,
|
||||
rate=100,
|
||||
stock_entry_type="Material Receipt",
|
||||
target="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
riv = frappe.get_all("Repost Item Valuation", filters={"voucher_no": stock_entry.name}, pluck="name")
|
||||
self.assertFalse(riv)
|
||||
|
||||
@@ -635,8 +635,10 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False):
|
||||
taxes_with_no_validity = []
|
||||
|
||||
for tax in taxes:
|
||||
tax_company = frappe.get_cached_value("Item Tax Template", tax.item_tax_template, "company")
|
||||
if tax_company == args["company"]:
|
||||
disabled, tax_company = frappe.get_cached_value(
|
||||
"Item Tax Template", tax.item_tax_template, ["disabled", "company"]
|
||||
)
|
||||
if not disabled and tax_company == args["company"]:
|
||||
if tax.valid_from or tax.maximum_net_rate:
|
||||
# In purchase Invoice first preference will be given to supplier invoice date
|
||||
# if supplier date is not present then posting date
|
||||
|
||||
@@ -159,10 +159,11 @@ def assign_item_groups_to_svd_list(svd_list: SVDList) -> None:
|
||||
|
||||
def get_item_groups_map(svd_list: SVDList) -> dict[str, str]:
|
||||
item_codes = set(i["item_code"] for i in svd_list)
|
||||
ig_list = frappe.get_list(
|
||||
"Item", fields=["item_code", "item_group"], filters=[("item_code", "in", item_codes)]
|
||||
return frappe._dict(
|
||||
frappe.get_all(
|
||||
"Item", fields=["name", "item_group"], filters=[("name", "in", item_codes)], as_list=True
|
||||
)
|
||||
)
|
||||
return {i["item_code"]: i["item_group"] for i in ig_list}
|
||||
|
||||
|
||||
def get_item_groups_dict() -> ItemGroupsDict:
|
||||
|
||||
@@ -274,14 +274,6 @@ class FIFOSlots:
|
||||
|
||||
d.actual_qty = d.bal_qty
|
||||
key, fifo_queue, transferred_item_key = self.__init_key_stores(d)
|
||||
serial_nos = d.serial_no if d.serial_no else []
|
||||
if fifo_queue and isinstance(fifo_queue[0][0], str):
|
||||
d.has_serial_no = 1
|
||||
|
||||
if d.actual_qty > 0:
|
||||
self.__compute_incoming_stock(d, fifo_queue, transferred_item_key, serial_nos)
|
||||
else:
|
||||
self.__compute_outgoing_stock(d, fifo_queue, transferred_item_key, serial_nos)
|
||||
|
||||
self.__update_balances(d, key)
|
||||
|
||||
@@ -439,10 +431,10 @@ class FIFOSlots:
|
||||
transfer_qty_to_pop = 0
|
||||
|
||||
def __update_balances(self, row: dict, key: tuple | str):
|
||||
self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction or row.bal_qty
|
||||
self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction or flt(row.bal_qty)
|
||||
|
||||
if "total_qty" not in self.item_details[key]:
|
||||
self.item_details[key]["total_qty"] = row.actual_qty or row.bal_qty
|
||||
self.item_details[key]["total_qty"] = row.actual_qty or flt(row.bal_qty)
|
||||
else:
|
||||
self.item_details[key]["total_qty"] += row.actual_qty
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt, today
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_pos_reserved_qty
|
||||
@@ -21,6 +22,10 @@ def execute(filters=None):
|
||||
columns = get_columns()
|
||||
bin_list = get_bin_list(filters)
|
||||
item_map = get_item_map(filters.get("item_code"), include_uom)
|
||||
item_groups = []
|
||||
if filters.get("item_group"):
|
||||
item_groups.append(filters.item_group)
|
||||
item_groups.extend(get_descendants_of("Item Group", filters.item_group))
|
||||
|
||||
warehouse_company = {}
|
||||
data = []
|
||||
@@ -40,7 +45,7 @@ def execute(filters=None):
|
||||
if filters.brand and filters.brand != item.brand:
|
||||
continue
|
||||
|
||||
elif filters.item_group and filters.item_group != item.item_group:
|
||||
elif item_groups and item.item_group not in item_groups:
|
||||
continue
|
||||
|
||||
elif filters.company and filters.company != company:
|
||||
|
||||
@@ -489,32 +489,19 @@ class update_entries_after:
|
||||
self.distinct_item_warehouses[key] = val
|
||||
self.new_items_found = True
|
||||
else:
|
||||
# Check if the dependent voucher is reposted
|
||||
# If not, then do not add it to the list
|
||||
if not self.is_dependent_voucher_reposted(dependant_sle):
|
||||
return
|
||||
|
||||
existing_sle_posting_date = self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
|
||||
|
||||
dependent_voucher_detail_nos = self.get_dependent_voucher_detail_nos(key)
|
||||
if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
|
||||
if dependent_voucher_detail_nos and dependant_sle.voucher_detail_no in set(
|
||||
dependent_voucher_detail_nos
|
||||
):
|
||||
existing_sle = self.distinct_item_warehouses[key].get("sle", {})
|
||||
if getdate(existing_sle.get("posting_date")) > getdate(dependant_sle.posting_date):
|
||||
self.distinct_item_warehouses[key] = val
|
||||
self.new_items_found = True
|
||||
elif dependant_sle.voucher_type == "Stock Entry" and is_transfer_stock_entry(
|
||||
dependant_sle.voucher_no
|
||||
):
|
||||
if self.distinct_item_warehouses[key].get("transfer_entry_to_repost"):
|
||||
return
|
||||
|
||||
val.sle_changed = True
|
||||
dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
|
||||
val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
|
||||
val["transfer_entry_to_repost"] = True
|
||||
self.distinct_item_warehouses[key] = val
|
||||
self.new_items_found = True
|
||||
elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos):
|
||||
# Future dependent voucher needs to be repost to get the correct stock value
|
||||
# If dependent voucher has not reposted, then add it to the list
|
||||
dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
|
||||
self.new_items_found = True
|
||||
val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
|
||||
self.distinct_item_warehouses[key] = val
|
||||
|
||||
def is_dependent_voucher_reposted(self, dependant_sle) -> bool:
|
||||
# Return False if the dependent voucher is not reposted
|
||||
@@ -655,11 +642,15 @@ class update_entries_after:
|
||||
if not sle.is_adjustment_entry:
|
||||
sle.stock_value_difference = stock_value_difference
|
||||
elif sle.is_adjustment_entry and not self.args.get("sle_id"):
|
||||
sle.stock_value_difference = get_stock_value_difference(
|
||||
sle.item_code, sle.warehouse, sle.posting_date, sle.posting_time, sle.voucher_no
|
||||
sle.stock_value_difference = (
|
||||
get_stock_value_difference(
|
||||
sle.item_code, sle.warehouse, sle.posting_date, sle.posting_time, sle.voucher_no
|
||||
)
|
||||
* -1
|
||||
)
|
||||
|
||||
sle.doctype = "Stock Ledger Entry"
|
||||
sle.modified = now()
|
||||
frappe.get_doc(sle).db_update()
|
||||
|
||||
if not self.args.get("sle_id"):
|
||||
@@ -890,12 +881,19 @@ class update_entries_after:
|
||||
|
||||
def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
|
||||
if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
|
||||
if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value(
|
||||
sle.voucher_type, sle.voucher_no, "is_internal_supplier"
|
||||
):
|
||||
frappe.db.set_value(
|
||||
f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate
|
||||
if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
details = frappe.get_cached_value(
|
||||
sle.voucher_type,
|
||||
sle.voucher_no,
|
||||
["is_internal_supplier", "is_return", "return_against"],
|
||||
as_dict=True,
|
||||
)
|
||||
if details.is_internal_supplier or (details.is_return and not details.return_against):
|
||||
rate = outgoing_rate if details.is_return else sle.outgoing_rate
|
||||
|
||||
frappe.db.set_value(
|
||||
f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", rate
|
||||
)
|
||||
else:
|
||||
frappe.db.set_value(
|
||||
"Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
|
||||
@@ -1392,6 +1390,8 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
|
||||
"posting_time",
|
||||
"voucher_detail_no",
|
||||
"posting_datetime as timestamp",
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
],
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -1819,3 +1819,10 @@ def get_stock_value_difference(item_code, warehouse, posting_date, posting_time,
|
||||
|
||||
difference_amount = query.run()
|
||||
return flt(difference_amount[0][0]) if difference_amount else 0
|
||||
|
||||
|
||||
@frappe.request_cache
|
||||
def is_transfer_stock_entry(voucher_no):
|
||||
purpose = frappe.get_cached_value("Stock Entry", voucher_no, "purpose")
|
||||
|
||||
return purpose in ["Material Transfer", "Material Transfer for Manufacture", "Send to Subcontractor"]
|
||||
|
||||
@@ -109,6 +109,8 @@ def get_stock_balance(
|
||||
|
||||
from erpnext.stock.stock_ledger import get_previous_sle
|
||||
|
||||
frappe.has_permission("Item", "read", throw=True)
|
||||
|
||||
if posting_date is None:
|
||||
posting_date = nowdate()
|
||||
if posting_time is None:
|
||||
@@ -127,8 +129,9 @@ def get_stock_balance(
|
||||
extra_cond = ""
|
||||
if inventory_dimensions_dict:
|
||||
for field, value in inventory_dimensions_dict.items():
|
||||
column = frappe.utils.sanitize_column(field)
|
||||
args[field] = value
|
||||
extra_cond += f" and {field} = %({field})s"
|
||||
extra_cond += f" and {column} = %({field})s"
|
||||
|
||||
last_entry = get_previous_sle(args, extra_cond=extra_cond)
|
||||
|
||||
@@ -275,8 +278,6 @@ def get_incoming_rate(args, raise_error_if_no_rate=True):
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
voucher_no = args.get("voucher_no") or args.get("name")
|
||||
|
||||
in_rate = None
|
||||
if (args.get("serial_no") or "").strip():
|
||||
in_rate = get_avg_purchase_rate(args.get("serial_no"))
|
||||
@@ -299,12 +300,13 @@ def get_incoming_rate(args, raise_error_if_no_rate=True):
|
||||
in_rate = (
|
||||
_get_fifo_lifo_rate(previous_stock_queue, args.get("qty") or 0, valuation_method)
|
||||
if previous_stock_queue
|
||||
else 0
|
||||
else None
|
||||
)
|
||||
elif valuation_method == "Moving Average":
|
||||
in_rate = previous_sle.get("valuation_rate") or 0
|
||||
in_rate = previous_sle.get("valuation_rate")
|
||||
|
||||
if in_rate is None:
|
||||
voucher_no = args.get("voucher_no") or args.get("name")
|
||||
in_rate = get_valuation_rate(
|
||||
args.get("item_code"),
|
||||
args.get("warehouse"),
|
||||
|
||||
Reference in New Issue
Block a user