mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-04 14:10:52 +00:00
Compare commits
242 Commits
remove_ret
...
add-einvoi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a16347f325 | ||
|
|
19b9875ba8 | ||
|
|
43aa670e90 | ||
|
|
4f76ed1c68 | ||
|
|
b93d13feef | ||
|
|
e1a32cc620 | ||
|
|
550f5f280c | ||
|
|
fbbfeb8563 | ||
|
|
418c131331 | ||
|
|
a8bf17e560 | ||
|
|
9961037f71 | ||
|
|
84ee1b86af | ||
|
|
afe86d83aa | ||
|
|
76e4bb44f1 | ||
|
|
c1bc1040b8 | ||
|
|
1d23c9a9fd | ||
|
|
5a211813d3 | ||
|
|
601b1e3821 | ||
|
|
a91483899b | ||
|
|
2f6d55d8d2 | ||
|
|
4a10454d89 | ||
|
|
12b15347bc | ||
|
|
9b63a1a2e9 | ||
|
|
8888957952 | ||
|
|
a9660bf026 | ||
|
|
b877b0d3a2 | ||
|
|
e4082093b8 | ||
|
|
9dc0edfb8a | ||
|
|
e13f05ce19 | ||
|
|
310e7c522c | ||
|
|
e32e0bc8fa | ||
|
|
0c24ba0118 | ||
|
|
60fa87751a | ||
|
|
50bc7785f7 | ||
|
|
d2aea0cd2e | ||
|
|
e9006582fb | ||
|
|
3483e0e2a5 | ||
|
|
65b047c94c | ||
|
|
ada3ce21c0 | ||
|
|
cb89dba5ab | ||
|
|
d73237e896 | ||
|
|
8e0d393cd4 | ||
|
|
292ff1bd6b | ||
|
|
804cf40f69 | ||
|
|
85f48dd6bd | ||
|
|
fa0a137a38 | ||
|
|
5fc8d20e96 | ||
|
|
7ddcfc00fc | ||
|
|
3f02059085 | ||
|
|
71b21086b1 | ||
|
|
c2817bed0b | ||
|
|
dc972718d5 | ||
|
|
7afb19f965 | ||
|
|
0ca137808b | ||
|
|
99e0106193 | ||
|
|
447485ae90 | ||
|
|
98bcb7255d | ||
|
|
2df24ec459 | ||
|
|
3a2f08fbda | ||
|
|
7c9b535514 | ||
|
|
4f0c2909ab | ||
|
|
1741501ea8 | ||
|
|
ea032893d3 | ||
|
|
7601a016b8 | ||
|
|
b5376ce5cb | ||
|
|
b8d97b82b8 | ||
|
|
56c4c3186b | ||
|
|
015a221d8d | ||
|
|
eec770a4a8 | ||
|
|
44f5ccc9dc | ||
|
|
0ae2a4f1c4 | ||
|
|
5fdaddad86 | ||
|
|
03ccddc1cc | ||
|
|
e00ebcd9e5 | ||
|
|
7e7122b668 | ||
|
|
b1c70af4db | ||
|
|
ff3bfb060a | ||
|
|
63e087d31e | ||
|
|
b62a397397 | ||
|
|
48efcc82b6 | ||
|
|
4383980bcc | ||
|
|
aa0552d788 | ||
|
|
3b5889ef85 | ||
|
|
8238b89907 | ||
|
|
6ae2f90683 | ||
|
|
cb7aef505d | ||
|
|
4a8c42d62b | ||
|
|
c67bdcf3c6 | ||
|
|
7a9e7b66ac | ||
|
|
151b9d4d7b | ||
|
|
b966f10711 | ||
|
|
0b48f13873 | ||
|
|
b531a38efe | ||
|
|
dd26ef96e0 | ||
|
|
227ce5f8a2 | ||
|
|
acd64ba7c1 | ||
|
|
6f43133c04 | ||
|
|
f72602ebf3 | ||
|
|
002ae8ae13 | ||
|
|
558dc57b94 | ||
|
|
cd942d36a8 | ||
|
|
3257533ee3 | ||
|
|
2763d06307 | ||
|
|
a0772ea8d7 | ||
|
|
d67b44fcec | ||
|
|
257a2a3d71 | ||
|
|
e6abbd1c83 | ||
|
|
41599cf29f | ||
|
|
997990c69f | ||
|
|
2bf76f64bd | ||
|
|
e1c41b9195 | ||
|
|
32a9575f07 | ||
|
|
e031e095dd | ||
|
|
39ce3756c8 | ||
|
|
f2515d9c9c | ||
|
|
27fe9c9179 | ||
|
|
503183cb0b | ||
|
|
f1ea5de022 | ||
|
|
baa4fec611 | ||
|
|
476175b307 | ||
|
|
3648745e5e | ||
|
|
8e1e6a194b | ||
|
|
663e9b403f | ||
|
|
3219b7c766 | ||
|
|
3a6e4c8da7 | ||
|
|
96a6db0422 | ||
|
|
4757a65863 | ||
|
|
0f8e34e972 | ||
|
|
2fd4485f2f | ||
|
|
299175e6fe | ||
|
|
8251b843ec | ||
|
|
60745705a8 | ||
|
|
caedc9f6ad | ||
|
|
fbeb86b9c0 | ||
|
|
ad43b18ace | ||
|
|
9f8d3e43ea | ||
|
|
9dbb2bf512 | ||
|
|
8c52c71299 | ||
|
|
c4c52c1cd3 | ||
|
|
b7fbf75e10 | ||
|
|
6dabfbedf1 | ||
|
|
68c592377b | ||
|
|
d69d2217f6 | ||
|
|
cdc8297083 | ||
|
|
f24f71f387 | ||
|
|
552c5951bd | ||
|
|
562025eb45 | ||
|
|
f0a37f545a | ||
|
|
001afb96ba | ||
|
|
61d3370527 | ||
|
|
32456bff30 | ||
|
|
1aed76f778 | ||
|
|
308c400c6a | ||
|
|
a9546dd01f | ||
|
|
cb4fbd5432 | ||
|
|
6f8d62088e | ||
|
|
5d66646fcd | ||
|
|
6a58d15497 | ||
|
|
6b21deedae | ||
|
|
7124328640 | ||
|
|
430a4c98a0 | ||
|
|
c65fb64578 | ||
|
|
14ab9d9158 | ||
|
|
1095052479 | ||
|
|
b4a511cbb4 | ||
|
|
0b2405bbdf | ||
|
|
255aa7a84a | ||
|
|
a604eed1b9 | ||
|
|
96fa14be88 | ||
|
|
ac8100f1e5 | ||
|
|
07cc05785e | ||
|
|
f03fbc0e6d | ||
|
|
447c553954 | ||
|
|
b2e9dccc8c | ||
|
|
9e8e7aab70 | ||
|
|
6c528d469d | ||
|
|
988c5b95e6 | ||
|
|
1f2887d601 | ||
|
|
ed4ac100ba | ||
|
|
a194d28b69 | ||
|
|
3217924242 | ||
|
|
02468a902f | ||
|
|
a13eecc961 | ||
|
|
776ee53a25 | ||
|
|
ddf5565c67 | ||
|
|
4152a9f026 | ||
|
|
8f961abe8b | ||
|
|
70c68f011a | ||
|
|
9066009e89 | ||
|
|
9332e7f96f | ||
|
|
d6a335e59f | ||
|
|
a66002f774 | ||
|
|
6eb3428f8e | ||
|
|
d158784472 | ||
|
|
80d046a38c | ||
|
|
d68cdb4f5e | ||
|
|
9f988910b5 | ||
|
|
09cd480c81 | ||
|
|
4777991a74 | ||
|
|
1301a6ff7f | ||
|
|
a6a5f63af2 | ||
|
|
1cfeb9371c | ||
|
|
638d5e9dc3 | ||
|
|
b4ee72e15e | ||
|
|
2a9b519773 | ||
|
|
57136fa921 | ||
|
|
882542c2d5 | ||
|
|
0a4025e7e0 | ||
|
|
54dfd50391 | ||
|
|
45d02ceb4e | ||
|
|
f72bb18da7 | ||
|
|
f371008cd9 | ||
|
|
82e5a784cb | ||
|
|
d267372334 | ||
|
|
92c7d074dd | ||
|
|
cedc8d28e4 | ||
|
|
d0bd78ddcd | ||
|
|
e2912caeae | ||
|
|
6f9c2e6c80 | ||
|
|
96bf1e2a0a | ||
|
|
1f633b293d | ||
|
|
0cd1cacc29 | ||
|
|
21154c8bee | ||
|
|
9270e58969 | ||
|
|
de8f44bbff | ||
|
|
8abfdb6598 | ||
|
|
305693b562 | ||
|
|
794eeebabc | ||
|
|
75768d780f | ||
|
|
fe252b48f7 | ||
|
|
ca0cce7599 | ||
|
|
e07fd46a46 | ||
|
|
51d7c0bfe4 | ||
|
|
4165fee6a7 | ||
|
|
d251ef9ce5 | ||
|
|
afda4bdb17 | ||
|
|
c4452360da | ||
|
|
13527d6154 | ||
|
|
5c0a46110e | ||
|
|
d9d6a07bbb | ||
|
|
e65990eb34 | ||
|
|
07a7fdbe6c |
2
.github/workflows/server-tests.yml
vendored
2
.github/workflows/server-tests.yml
vendored
@@ -93,7 +93,7 @@ jobs:
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
|
||||
- name: Run Tests
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator
|
||||
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --total-builds 2 --build-number ${{ matrix.container }}'
|
||||
env:
|
||||
TYPE: server
|
||||
CI_BUILD_ID: ${{ github.run_id }}
|
||||
|
||||
18
CODEOWNERS
18
CODEOWNERS
@@ -12,17 +12,13 @@ erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/support/ @nextchamp-saqib @deepeshgarg007
|
||||
pos* @nextchamp-saqib
|
||||
|
||||
erpnext/buying/ @marination @rohitwaghchaure @s-aga-r
|
||||
erpnext/e_commerce/ @marination
|
||||
erpnext/maintenance/ @marination @rohitwaghchaure @s-aga-r
|
||||
erpnext/manufacturing/ @marination @rohitwaghchaure @s-aga-r
|
||||
erpnext/portal/ @marination
|
||||
erpnext/quality_management/ @marination @rohitwaghchaure @s-aga-r
|
||||
erpnext/shopping_cart/ @marination
|
||||
erpnext/stock/ @marination @rohitwaghchaure @s-aga-r
|
||||
erpnext/buying/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/maintenance/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/manufacturing/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/quality_management/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/stock/ @rohitwaghchaure @s-aga-r
|
||||
|
||||
|
||||
erpnext/crm/ @NagariaHussain
|
||||
erpnext/education/ @rutwikhdev
|
||||
erpnext/healthcare/ @chillaranand
|
||||
erpnext/hr/ @ruchamahabal
|
||||
erpnext/non_profit/ @ruchamahabal
|
||||
@@ -30,7 +26,7 @@ erpnext/payroll @ruchamahabal
|
||||
erpnext/projects/ @ruchamahabal
|
||||
|
||||
erpnext/controllers @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination
|
||||
erpnext/patches/ @deepeshgarg007 @nextchamp-saqib @marination rohitwaghchaure
|
||||
erpnext/patches/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure
|
||||
erpnext/public/ @nextchamp-saqib @marination
|
||||
|
||||
.github/ @ankush
|
||||
|
||||
@@ -173,8 +173,8 @@ frappe.ui.form.on("Journal Entry", {
|
||||
var update_jv_details = function(doc, r) {
|
||||
$.each(r, function(i, d) {
|
||||
var row = frappe.model.add_child(doc, "Journal Entry Account", "accounts");
|
||||
row.account = d.account;
|
||||
row.balance = d.balance;
|
||||
frappe.model.set_value(row.doctype, row.name, "account", d.account)
|
||||
frappe.model.set_value(row.doctype, row.name, "balance", d.balance)
|
||||
});
|
||||
refresh_field("accounts");
|
||||
}
|
||||
|
||||
@@ -194,7 +194,9 @@ class JournalEntry(AccountsController):
|
||||
}
|
||||
)
|
||||
|
||||
tax_withholding_details = get_party_tax_withholding_details(inv, self.tax_withholding_category)
|
||||
tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details(
|
||||
inv, self.tax_withholding_category
|
||||
)
|
||||
|
||||
if not tax_withholding_details:
|
||||
return
|
||||
|
||||
@@ -1111,7 +1111,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
|
||||
$.each(tax_fields, function(i, fieldname) { tax[fieldname] = 0.0; });
|
||||
|
||||
frm.doc.paid_amount_after_tax = frm.doc.paid_amount;
|
||||
frm.doc.paid_amount_after_tax = frm.doc.base_paid_amount;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1202,7 +1202,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
|
||||
cumulated_tax_fraction += tax.tax_fraction_for_current_item;
|
||||
frm.doc.paid_amount_after_tax = flt(frm.doc.paid_amount/(1+cumulated_tax_fraction))
|
||||
frm.doc.paid_amount_after_tax = flt(frm.doc.base_paid_amount/(1+cumulated_tax_fraction))
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1234,6 +1234,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.doc.total_taxes_and_charges = 0.0;
|
||||
frm.doc.base_total_taxes_and_charges = 0.0;
|
||||
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
let actual_tax_dict = {};
|
||||
|
||||
// maintain actual tax rate based on idx
|
||||
@@ -1254,8 +1255,8 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
}
|
||||
|
||||
tax.tax_amount = current_tax_amount;
|
||||
tax.base_tax_amount = tax.tax_amount * frm.doc.source_exchange_rate;
|
||||
// tax accounts are only in company currency
|
||||
tax.base_tax_amount = current_tax_amount;
|
||||
current_tax_amount *= (tax.add_deduct_tax == "Deduct") ? -1.0 : 1.0;
|
||||
|
||||
if(i==0) {
|
||||
@@ -1264,9 +1265,29 @@ frappe.ui.form.on('Payment Entry', {
|
||||
tax.total = flt(frm.doc["taxes"][i-1].total + current_tax_amount, precision("total", tax));
|
||||
}
|
||||
|
||||
tax.base_total = tax.total * frm.doc.source_exchange_rate;
|
||||
frm.doc.total_taxes_and_charges += current_tax_amount;
|
||||
frm.doc.base_total_taxes_and_charges += current_tax_amount * frm.doc.source_exchange_rate;
|
||||
// tac accounts are only in company currency
|
||||
tax.base_total = tax.total
|
||||
|
||||
// calculate total taxes and base total taxes
|
||||
if(frm.doc.payment_type == "Pay") {
|
||||
// tax accounts only have company currency
|
||||
if(tax.currency != frm.doc.paid_to_account_currency) {
|
||||
//total_taxes_and_charges has the target currency. so using target conversion rate
|
||||
frm.doc.total_taxes_and_charges += flt(current_tax_amount / frm.doc.target_exchange_rate);
|
||||
|
||||
} else {
|
||||
frm.doc.total_taxes_and_charges += current_tax_amount;
|
||||
}
|
||||
} else if(frm.doc.payment_type == "Receive") {
|
||||
if(tax.currency != frm.doc.paid_from_account_currency) {
|
||||
//total_taxes_and_charges has the target currency. so using source conversion rate
|
||||
frm.doc.total_taxes_and_charges += flt(current_tax_amount / frm.doc.source_exchange_rate);
|
||||
} else {
|
||||
frm.doc.total_taxes_and_charges += current_tax_amount;
|
||||
}
|
||||
}
|
||||
|
||||
frm.doc.base_total_taxes_and_charges += tax.base_tax_amount;
|
||||
|
||||
frm.refresh_field('taxes');
|
||||
frm.refresh_field('total_taxes_and_charges');
|
||||
|
||||
@@ -944,6 +944,13 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
|
||||
if not d.included_in_paid_amount:
|
||||
if get_account_currency(payment_account) != self.company_currency:
|
||||
if self.payment_type == "Receive":
|
||||
exchange_rate = self.target_exchange_rate
|
||||
elif self.payment_type in ["Pay", "Internal Transfer"]:
|
||||
exchange_rate = self.source_exchange_rate
|
||||
base_tax_amount = flt((tax_amount / exchange_rate), self.precision("paid_amount"))
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
@@ -1059,7 +1066,7 @@ class PaymentEntry(AccountsController):
|
||||
for fieldname in tax_fields:
|
||||
tax.set(fieldname, 0.0)
|
||||
|
||||
self.paid_amount_after_tax = self.paid_amount
|
||||
self.paid_amount_after_tax = self.base_paid_amount
|
||||
|
||||
def determine_exclusive_rate(self):
|
||||
if not any((cint(tax.included_in_paid_amount) for tax in self.get("taxes"))):
|
||||
@@ -1078,7 +1085,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
cumulated_tax_fraction += tax.tax_fraction_for_current_item
|
||||
|
||||
self.paid_amount_after_tax = flt(self.paid_amount / (1 + cumulated_tax_fraction))
|
||||
self.paid_amount_after_tax = flt(self.base_paid_amount / (1 + cumulated_tax_fraction))
|
||||
|
||||
def calculate_taxes(self):
|
||||
self.total_taxes_and_charges = 0.0
|
||||
@@ -1101,7 +1108,7 @@ class PaymentEntry(AccountsController):
|
||||
current_tax_amount += actual_tax_dict[tax.idx]
|
||||
|
||||
tax.tax_amount = current_tax_amount
|
||||
tax.base_tax_amount = tax.tax_amount * self.source_exchange_rate
|
||||
tax.base_tax_amount = current_tax_amount
|
||||
|
||||
if tax.add_deduct_tax == "Deduct":
|
||||
current_tax_amount *= -1.0
|
||||
@@ -1115,14 +1122,20 @@ class PaymentEntry(AccountsController):
|
||||
self.get("taxes")[i - 1].total + current_tax_amount, self.precision("total", tax)
|
||||
)
|
||||
|
||||
tax.base_total = tax.total * self.source_exchange_rate
|
||||
tax.base_total = tax.total
|
||||
|
||||
if self.payment_type == "Pay":
|
||||
self.base_total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
|
||||
self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
|
||||
else:
|
||||
self.base_total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
|
||||
self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
|
||||
if tax.currency != self.paid_to_account_currency:
|
||||
self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
|
||||
else:
|
||||
self.total_taxes_and_charges += current_tax_amount
|
||||
elif self.payment_type == "Receive":
|
||||
if tax.currency != self.paid_from_account_currency:
|
||||
self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
|
||||
else:
|
||||
self.total_taxes_and_charges += current_tax_amount
|
||||
|
||||
self.base_total_taxes_and_charges += tax.base_tax_amount
|
||||
|
||||
if self.get("taxes"):
|
||||
self.paid_amount_after_tax = self.get("taxes")[-1].base_total
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.utils import flt, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
@@ -743,6 +744,46 @@ class TestPaymentEntry(unittest.TestCase):
|
||||
flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2)
|
||||
)
|
||||
|
||||
def test_gl_of_multi_currency_payment_with_taxes(self):
|
||||
payment_entry = create_payment_entry(
|
||||
party="_Test Supplier USD", paid_to="_Test Payable USD - _TC", save=True
|
||||
)
|
||||
payment_entry.append(
|
||||
"taxes",
|
||||
{
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"charge_type": "Actual",
|
||||
"tax_amount": 100,
|
||||
"add_deduct_tax": "Add",
|
||||
"description": "Test",
|
||||
},
|
||||
)
|
||||
payment_entry.target_exchange_rate = 80
|
||||
payment_entry.received_amount = 12.5
|
||||
payment_entry = payment_entry.submit()
|
||||
gle = qb.DocType("GL Entry")
|
||||
gl_entries = (
|
||||
qb.from_(gle)
|
||||
.select(
|
||||
gle.account,
|
||||
gle.debit,
|
||||
gle.credit,
|
||||
gle.debit_in_account_currency,
|
||||
gle.credit_in_account_currency,
|
||||
)
|
||||
.orderby(gle.account)
|
||||
.where(gle.voucher_no == payment_entry.name)
|
||||
.run()
|
||||
)
|
||||
|
||||
expected_gl_entries = (
|
||||
("_Test Account Service Tax - _TC", 100.0, 0.0, 100.0, 0.0),
|
||||
("_Test Bank - _TC", 0.0, 1100.0, 0.0, 1100.0),
|
||||
("_Test Payable USD - _TC", 1000.0, 0.0, 12.5, 0),
|
||||
)
|
||||
|
||||
self.assertEqual(gl_entries, expected_gl_entries)
|
||||
|
||||
def test_payment_entry_against_onhold_purchase_invoice(self):
|
||||
pi = make_purchase_invoice()
|
||||
|
||||
|
||||
@@ -8,7 +8,11 @@ from frappe.model.document import Document
|
||||
from frappe.utils import flt, getdate, nowdate, today
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.utils import get_outstanding_invoices, reconcile_against_document
|
||||
from erpnext.accounts.utils import (
|
||||
get_outstanding_invoices,
|
||||
reconcile_against_document,
|
||||
update_reference_in_payment_entry,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import get_advance_payment_entries
|
||||
|
||||
|
||||
@@ -190,6 +194,23 @@ class PaymentReconciliation(Document):
|
||||
inv.currency = entry.get("currency")
|
||||
inv.outstanding_amount = flt(entry.get("outstanding_amount"))
|
||||
|
||||
def get_difference_amount(self, allocated_entry):
|
||||
if allocated_entry.get("reference_type") != "Payment Entry":
|
||||
return
|
||||
|
||||
dr_or_cr = (
|
||||
"credit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
else "debit_in_account_currency"
|
||||
)
|
||||
|
||||
row = self.get_payment_details(allocated_entry, dr_or_cr)
|
||||
|
||||
doc = frappe.get_doc(allocated_entry.reference_type, allocated_entry.reference_name)
|
||||
update_reference_in_payment_entry(row, doc, do_not_save=True)
|
||||
|
||||
return doc.difference_amount
|
||||
|
||||
@frappe.whitelist()
|
||||
def allocate_entries(self, args):
|
||||
self.validate_entries()
|
||||
@@ -205,12 +226,16 @@ class PaymentReconciliation(Document):
|
||||
res = self.get_allocated_entry(pay, inv, pay["amount"])
|
||||
inv["outstanding_amount"] = flt(inv.get("outstanding_amount")) - flt(pay.get("amount"))
|
||||
pay["amount"] = 0
|
||||
|
||||
res.difference_amount = self.get_difference_amount(res)
|
||||
|
||||
if pay.get("amount") == 0:
|
||||
entries.append(res)
|
||||
break
|
||||
elif inv.get("outstanding_amount") == 0:
|
||||
entries.append(res)
|
||||
continue
|
||||
|
||||
else:
|
||||
break
|
||||
|
||||
@@ -332,7 +357,7 @@ class PaymentReconciliation(Document):
|
||||
def get_conditions(self, get_invoices=False, get_payments=False, get_return_invoices=False):
|
||||
condition = " and company = '{0}' ".format(self.company)
|
||||
|
||||
if self.get("cost_center") and (get_invoices or get_payments or get_return_invoices):
|
||||
if self.get("cost_center"):
|
||||
condition = " and cost_center = '{0}' ".format(self.cost_center)
|
||||
|
||||
if get_invoices:
|
||||
|
||||
@@ -186,8 +186,10 @@
|
||||
{
|
||||
"fetch_from": "bank_account.bank",
|
||||
"fieldname": "bank",
|
||||
"fieldtype": "Read Only",
|
||||
"label": "Bank"
|
||||
"fieldtype": "Link",
|
||||
"label": "Bank",
|
||||
"options": "Bank",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "bank_account.bank_account_no",
|
||||
@@ -366,10 +368,11 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-18 12:24:14.178853",
|
||||
"modified": "2022-09-30 16:19:43.680025",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Request",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -401,5 +404,6 @@
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -39,6 +39,7 @@
|
||||
{
|
||||
"columns": 2,
|
||||
"fetch_from": "payment_term.description",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"in_list_view": 1,
|
||||
@@ -159,7 +160,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-28 05:41:35.084233",
|
||||
"modified": "2022-09-16 13:57:06.382859",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Schedule",
|
||||
@@ -168,5 +169,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1572,7 +1572,7 @@
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-03-22 13:00:24.166684",
|
||||
"modified": "2022-09-27 13:00:24.166684",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
|
||||
@@ -239,14 +239,14 @@ class POSInvoice(SalesInvoice):
|
||||
frappe.bold(d.warehouse),
|
||||
frappe.bold(d.qty),
|
||||
)
|
||||
if flt(available_stock) <= 0:
|
||||
if is_stock_item and flt(available_stock) <= 0:
|
||||
frappe.throw(
|
||||
_("Row #{}: Item Code: {} is not available under warehouse {}.").format(
|
||||
d.idx, item_code, warehouse
|
||||
),
|
||||
title=_("Item Unavailable"),
|
||||
)
|
||||
elif flt(available_stock) < flt(d.qty):
|
||||
elif is_stock_item and flt(available_stock) < flt(d.qty):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}."
|
||||
@@ -634,11 +634,12 @@ def get_stock_availability(item_code, warehouse):
|
||||
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
|
||||
return bin_qty - pos_sales_qty, is_stock_item
|
||||
else:
|
||||
is_stock_item = False
|
||||
is_stock_item = True
|
||||
if frappe.db.exists("Product Bundle", item_code):
|
||||
return get_bundle_availability(item_code, warehouse), is_stock_item
|
||||
else:
|
||||
# Is a service item
|
||||
is_stock_item = False
|
||||
# Is a service item or non_stock item
|
||||
return 0, is_stock_item
|
||||
|
||||
|
||||
@@ -652,7 +653,9 @@ def get_bundle_availability(bundle_item_code, warehouse):
|
||||
available_qty = item_bin_qty - item_pos_reserved_qty
|
||||
|
||||
max_available_bundles = available_qty / item.qty
|
||||
if bundle_bin_qty > max_available_bundles:
|
||||
if bundle_bin_qty > max_available_bundles and frappe.get_value(
|
||||
"Item", item.item_code, "is_stock_item"
|
||||
):
|
||||
bundle_bin_qty = max_available_bundles
|
||||
|
||||
pos_sales_qty = get_pos_reserved_qty(bundle_item_code, warehouse)
|
||||
@@ -744,3 +747,7 @@ def add_return_modes(doc, pos_profile):
|
||||
]:
|
||||
payment_mode = get_mode_of_payment_info(mode_of_payment, doc.company)
|
||||
append_payment(payment_mode[0])
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("POS Invoice", ["return_against"])
|
||||
|
||||
@@ -495,6 +495,67 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pos.submit)
|
||||
|
||||
def test_value_error_on_serial_no_validation(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
||||
|
||||
se = make_serialized_item(
|
||||
company="_Test Company",
|
||||
target_warehouse="Stores - _TC",
|
||||
cost_center="Main - _TC",
|
||||
expense_account="Cost of Goods Sold - _TC",
|
||||
)
|
||||
serial_nos = se.get("items")[0].serial_no
|
||||
|
||||
# make a pos invoice
|
||||
pos = create_pos_invoice(
|
||||
company="_Test Company",
|
||||
debit_to="Debtors - _TC",
|
||||
account_for_change_amount="Cash - _TC",
|
||||
warehouse="Stores - _TC",
|
||||
income_account="Sales - _TC",
|
||||
expense_account="Cost of Goods Sold - _TC",
|
||||
cost_center="Main - _TC",
|
||||
item=se.get("items")[0].item_code,
|
||||
rate=1000,
|
||||
qty=1,
|
||||
do_not_save=1,
|
||||
)
|
||||
pos.get("items")[0].has_serial_no = 1
|
||||
pos.get("items")[0].serial_no = serial_nos.split("\n")[0]
|
||||
pos.set("payments", [])
|
||||
pos.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1}
|
||||
)
|
||||
pos = pos.save().submit()
|
||||
|
||||
# make a return
|
||||
pos_return = make_sales_return(pos.name)
|
||||
pos_return.paid_amount = pos_return.grand_total
|
||||
pos_return.save()
|
||||
pos_return.submit()
|
||||
|
||||
# set docstatus to 2 for pos to trigger this issue
|
||||
frappe.db.set_value("POS Invoice", pos.name, "docstatus", 2)
|
||||
|
||||
pos2 = create_pos_invoice(
|
||||
company="_Test Company",
|
||||
debit_to="Debtors - _TC",
|
||||
account_for_change_amount="Cash - _TC",
|
||||
warehouse="Stores - _TC",
|
||||
income_account="Sales - _TC",
|
||||
expense_account="Cost of Goods Sold - _TC",
|
||||
cost_center="Main - _TC",
|
||||
item=se.get("items")[0].item_code,
|
||||
rate=1000,
|
||||
qty=1,
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
pos2.get("items")[0].has_serial_no = 1
|
||||
pos2.get("items")[0].serial_no = serial_nos.split("\n")[0]
|
||||
# Value error should not be triggered on validation
|
||||
pos2.save()
|
||||
|
||||
def test_loyalty_points(self):
|
||||
from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
|
||||
get_loyalty_program_details_with_points,
|
||||
|
||||
@@ -269,6 +269,18 @@ def get_serial_no_for_item(args):
|
||||
return item_details
|
||||
|
||||
|
||||
def update_pricing_rule_uom(pricing_rule, args):
|
||||
child_doc = {"Item Code": "items", "Item Group": "item_groups", "Brand": "brands"}.get(
|
||||
pricing_rule.apply_on
|
||||
)
|
||||
|
||||
apply_on_field = frappe.scrub(pricing_rule.apply_on)
|
||||
|
||||
for row in pricing_rule.get(child_doc):
|
||||
if row.get(apply_on_field) == args.get(apply_on_field):
|
||||
pricing_rule.uom = row.uom
|
||||
|
||||
|
||||
def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=False):
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import (
|
||||
get_applied_pricing_rules,
|
||||
@@ -325,7 +337,8 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
|
||||
|
||||
if isinstance(pricing_rule, string_types):
|
||||
pricing_rule = frappe.get_cached_doc("Pricing Rule", pricing_rule)
|
||||
pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule)
|
||||
update_pricing_rule_uom(pricing_rule, args)
|
||||
pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule) or []
|
||||
|
||||
if pricing_rule.get("suggestion"):
|
||||
continue
|
||||
@@ -439,12 +452,15 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
|
||||
if pricing_rule.currency == args.currency:
|
||||
pricing_rule_rate = pricing_rule.rate
|
||||
|
||||
# TODO https://github.com/frappe/erpnext/pull/23636 solve this in some other way.
|
||||
if pricing_rule_rate:
|
||||
is_blank_uom = pricing_rule.get("uom") != args.get("uom")
|
||||
# Override already set price list rate (from item price)
|
||||
# if pricing_rule_rate > 0
|
||||
item_details.update(
|
||||
{
|
||||
"price_list_rate": pricing_rule_rate * args.get("conversion_factor", 1),
|
||||
"price_list_rate": pricing_rule_rate
|
||||
* (args.get("conversion_factor", 1) if is_blank_uom else 1),
|
||||
}
|
||||
)
|
||||
item_details.update({"discount_percentage": 0.0})
|
||||
|
||||
@@ -597,6 +597,121 @@ class TestPricingRule(unittest.TestCase):
|
||||
frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete()
|
||||
item.delete()
|
||||
|
||||
def test_item_price_with_blank_uom_pricing_rule(self):
|
||||
properties = {
|
||||
"item_code": "Item Blank UOM",
|
||||
"stock_uom": "Nos",
|
||||
"sales_uom": "Box",
|
||||
"uoms": [dict(uom="Box", conversion_factor=10)],
|
||||
}
|
||||
item = make_item(properties=properties)
|
||||
|
||||
make_item_price("Item Blank UOM", "_Test Price List", 100)
|
||||
|
||||
pricing_rule_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Item Blank UOM Rule",
|
||||
"apply_on": "Item Code",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "Item Blank UOM",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"currency": "INR",
|
||||
"rate_or_discount": "Rate",
|
||||
"rate": 101,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
rule = frappe.get_doc(pricing_rule_record)
|
||||
rule.insert()
|
||||
|
||||
si = create_sales_invoice(
|
||||
do_not_save=True, item_code="Item Blank UOM", uom="Box", conversion_factor=10
|
||||
)
|
||||
si.selling_price_list = "_Test Price List"
|
||||
si.save()
|
||||
|
||||
# If UOM is blank consider it as stock UOM and apply pricing_rule on all UOM.
|
||||
# rate is 101, Selling UOM is Box that have conversion_factor of 10 so 101 * 10 = 1010
|
||||
self.assertEqual(si.items[0].price_list_rate, 1010)
|
||||
self.assertEqual(si.items[0].rate, 1010)
|
||||
|
||||
si.delete()
|
||||
|
||||
si = create_sales_invoice(do_not_save=True, item_code="Item Blank UOM", uom="Nos")
|
||||
si.selling_price_list = "_Test Price List"
|
||||
si.save()
|
||||
|
||||
# UOM is blank so consider it as stock UOM and apply pricing_rule on all UOM.
|
||||
# rate is 101, Selling UOM is Nos that have conversion_factor of 1 so 101 * 1 = 101
|
||||
self.assertEqual(si.items[0].price_list_rate, 101)
|
||||
self.assertEqual(si.items[0].rate, 101)
|
||||
|
||||
si.delete()
|
||||
rule.delete()
|
||||
frappe.get_doc("Item Price", {"item_code": "Item Blank UOM"}).delete()
|
||||
|
||||
item.delete()
|
||||
|
||||
def test_item_price_with_selling_uom_pricing_rule(self):
|
||||
properties = {
|
||||
"item_code": "Item UOM other than Stock",
|
||||
"stock_uom": "Nos",
|
||||
"sales_uom": "Box",
|
||||
"uoms": [dict(uom="Box", conversion_factor=10)],
|
||||
}
|
||||
item = make_item(properties=properties)
|
||||
|
||||
make_item_price("Item UOM other than Stock", "_Test Price List", 100)
|
||||
|
||||
pricing_rule_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Item UOM other than Stock Rule",
|
||||
"apply_on": "Item Code",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "Item UOM other than Stock",
|
||||
"uom": "Box",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"currency": "INR",
|
||||
"rate_or_discount": "Rate",
|
||||
"rate": 101,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
rule = frappe.get_doc(pricing_rule_record)
|
||||
rule.insert()
|
||||
|
||||
si = create_sales_invoice(
|
||||
do_not_save=True, item_code="Item UOM other than Stock", uom="Box", conversion_factor=10
|
||||
)
|
||||
si.selling_price_list = "_Test Price List"
|
||||
si.save()
|
||||
|
||||
# UOM is Box so apply pricing_rule only on Box UOM.
|
||||
# Selling UOM is Box and as both UOM are same no need to multiply by conversion_factor.
|
||||
self.assertEqual(si.items[0].price_list_rate, 101)
|
||||
self.assertEqual(si.items[0].rate, 101)
|
||||
|
||||
si.delete()
|
||||
|
||||
si = create_sales_invoice(do_not_save=True, item_code="Item UOM other than Stock", uom="Nos")
|
||||
si.selling_price_list = "_Test Price List"
|
||||
si.save()
|
||||
|
||||
# UOM is Box so pricing_rule won't apply as selling_uom is Nos.
|
||||
# As Pricing Rule is not applied price of 100 will be fetched from Item Price List.
|
||||
self.assertEqual(si.items[0].price_list_rate, 100)
|
||||
self.assertEqual(si.items[0].rate, 100)
|
||||
|
||||
si.delete()
|
||||
rule.delete()
|
||||
frappe.get_doc("Item Price", {"item_code": "Item UOM other than Stock"}).delete()
|
||||
|
||||
item.delete()
|
||||
|
||||
def test_pricing_rule_for_different_currency(self):
|
||||
make_item("Test Sanitizer Item")
|
||||
|
||||
|
||||
@@ -111,6 +111,12 @@ def _get_pricing_rules(apply_on, args, values):
|
||||
)
|
||||
|
||||
if apply_on_field == "item_code":
|
||||
if args.get("uom", None):
|
||||
item_conditions += (
|
||||
" and ({child_doc}.uom='{item_uom}' or IFNULL({child_doc}.uom, '')='')".format(
|
||||
child_doc=child_doc, item_uom=args.get("uom")
|
||||
)
|
||||
)
|
||||
if "variant_of" not in args:
|
||||
args.variant_of = frappe.get_cached_value("Item", args.item_code, "variant_of")
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<table class="table table-bordered" style="font-size: 10px">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 12%">{{ _("Date") }}</th>
|
||||
|
||||
@@ -31,7 +31,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
|
||||
this._super();
|
||||
|
||||
// Ignore linked advances
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry'];
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice'];
|
||||
|
||||
if(!this.frm.doc.__islocal) {
|
||||
// show credit_to in print format
|
||||
@@ -569,6 +569,10 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
erpnext.queries.setup_queries(frm, "Warehouse", function() {
|
||||
return erpnext.queries.warehouse(frm.doc);
|
||||
});
|
||||
|
||||
if (frm.is_new()) {
|
||||
frm.clear_table("tax_withheld_vouchers");
|
||||
}
|
||||
},
|
||||
|
||||
is_subcontracted: function(frm) {
|
||||
|
||||
@@ -83,6 +83,8 @@
|
||||
"section_break_51",
|
||||
"taxes_and_charges",
|
||||
"taxes",
|
||||
"tax_withheld_vouchers_section",
|
||||
"tax_withheld_vouchers",
|
||||
"sec_tax_breakup",
|
||||
"other_charges_calculation",
|
||||
"totals",
|
||||
@@ -511,7 +513,6 @@
|
||||
"fieldname": "ignore_pricing_rule",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Pricing Rule",
|
||||
"no_copy": 1,
|
||||
"permlevel": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
@@ -1417,13 +1418,26 @@
|
||||
"label": "Advance Tax",
|
||||
"options": "Advance Tax",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withheld_vouchers_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Tax Withheld Vouchers"
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withheld_vouchers",
|
||||
"fieldtype": "Table",
|
||||
"label": "Tax Withheld Vouchers",
|
||||
"no_copy": 1,
|
||||
"options": "Tax Withheld Vouchers",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-25 13:31:02.716727",
|
||||
"modified": "2022-10-07 14:19:14.214157",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
@@ -1483,7 +1497,8 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"timeline_field": "supplier",
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,9 @@ class PurchaseInvoice(BuyingController):
|
||||
supplier_tds = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
|
||||
self.set_onload("supplier_tds", supplier_tds)
|
||||
|
||||
if self.is_new():
|
||||
self.set("tax_withheld_vouchers", [])
|
||||
|
||||
def before_save(self):
|
||||
if not self.on_hold:
|
||||
self.release_date = ""
|
||||
@@ -695,6 +698,10 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
)
|
||||
|
||||
credit_amount = item.base_net_amount
|
||||
if self.is_internal_supplier and item.valuation_rate:
|
||||
credit_amount = flt(item.valuation_rate * item.stock_qty)
|
||||
|
||||
# Intentionally passed negative debit amount to avoid incorrect GL Entry validation
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
@@ -704,7 +711,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"debit": -1 * flt(item.base_net_amount, item.precision("base_net_amount")),
|
||||
"debit": -1 * flt(credit_amount, item.precision("base_net_amount")),
|
||||
},
|
||||
warehouse_account[item.from_warehouse]["account_currency"],
|
||||
item=item,
|
||||
@@ -1364,7 +1371,14 @@ class PurchaseInvoice(BuyingController):
|
||||
frappe.db.set(self, "status", "Cancelled")
|
||||
|
||||
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
|
||||
|
||||
self.ignore_linked_doctypes = (
|
||||
"GL Entry",
|
||||
"Stock Ledger Entry",
|
||||
"Repost Item Valuation",
|
||||
"Tax Withheld Vouchers",
|
||||
)
|
||||
|
||||
self.update_advance_tax_references(cancel=1)
|
||||
|
||||
def update_project(self):
|
||||
@@ -1457,7 +1471,7 @@ class PurchaseInvoice(BuyingController):
|
||||
if not self.tax_withholding_category:
|
||||
return
|
||||
|
||||
tax_withholding_details, advance_taxes = get_party_tax_withholding_details(
|
||||
tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details(
|
||||
self, self.tax_withholding_category
|
||||
)
|
||||
|
||||
@@ -1486,6 +1500,19 @@ class PurchaseInvoice(BuyingController):
|
||||
for d in to_remove:
|
||||
self.remove(d)
|
||||
|
||||
## Add pending vouchers on which tax was withheld
|
||||
self.set("tax_withheld_vouchers", [])
|
||||
|
||||
for voucher_no, voucher_details in voucher_wise_amount.items():
|
||||
self.append(
|
||||
"tax_withheld_vouchers",
|
||||
{
|
||||
"voucher_name": voucher_no,
|
||||
"voucher_type": voucher_details.get("voucher_type"),
|
||||
"taxable_amount": voucher_details.get("amount"),
|
||||
},
|
||||
)
|
||||
|
||||
# calculate totals again after applying TDS
|
||||
self.calculate_taxes_and_totals()
|
||||
|
||||
|
||||
@@ -1549,6 +1549,37 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
pi.save()
|
||||
self.assertEqual(pi.items[0].conversion_factor, 1000)
|
||||
|
||||
def test_batch_expiry_for_purchase_invoice(self):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
|
||||
item = self.make_item(
|
||||
"_Test Batch Item For Return Check",
|
||||
{
|
||||
"is_purchase_item": 1,
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TBIRC.#####",
|
||||
},
|
||||
)
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
qty=1,
|
||||
item_code=item.name,
|
||||
update_stock=True,
|
||||
)
|
||||
|
||||
pi.load_from_db()
|
||||
batch_no = pi.items[0].batch_no
|
||||
self.assertTrue(batch_no)
|
||||
|
||||
frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(nowdate(), -1))
|
||||
|
||||
return_pi = make_return_doc(pi.doctype, pi.name)
|
||||
return_pi.save().submit()
|
||||
|
||||
self.assertTrue(return_pi.docstatus == 1)
|
||||
|
||||
|
||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
gl_entries = frappe.db.sql(
|
||||
|
||||
@@ -706,6 +706,7 @@
|
||||
"label": "Valuation Rate",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"precision": "6",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -871,7 +872,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-15 17:04:07.191013",
|
||||
"modified": "2022-10-12 03:37:29.032732",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
||||
@@ -651,7 +651,6 @@
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Ignore Pricing Rule",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
@@ -2046,7 +2045,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2022-07-11 17:43:56.435382",
|
||||
"modified": "2022-09-16 17:44:22.227332",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -2187,6 +2187,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
update_address(
|
||||
target_doc, "shipping_address", "shipping_address_display", source_doc.customer_address
|
||||
)
|
||||
update_address(
|
||||
target_doc, "billing_address", "billing_address_display", source_doc.customer_address
|
||||
)
|
||||
|
||||
if currency:
|
||||
target_doc.currency = currency
|
||||
|
||||
@@ -7,7 +7,7 @@ import unittest
|
||||
import frappe
|
||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||
from frappe.model.naming import make_autoname
|
||||
from frappe.utils import add_days, flt, getdate, nowdate
|
||||
from frappe.utils import add_days, flt, getdate, nowdate, today
|
||||
from six import iteritems
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
||||
@@ -31,10 +31,20 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
||||
get_qty_after_transaction,
|
||||
make_stock_entry,
|
||||
)
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
create_stock_reconciliation,
|
||||
)
|
||||
from erpnext.stock.utils import get_incoming_rate, get_stock_balance
|
||||
|
||||
|
||||
class TestSalesInvoice(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from erpnext.stock.doctype.stock_ledger_entry.test_stock_ledger_entry import create_items
|
||||
|
||||
create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}])
|
||||
create_internal_parties()
|
||||
setup_accounts()
|
||||
|
||||
def make(self):
|
||||
w = frappe.copy_doc(test_records[0])
|
||||
w.is_pos = 0
|
||||
@@ -906,7 +916,8 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
pos_return.insert()
|
||||
pos_return.submit()
|
||||
|
||||
self.assertEqual(pos_return.get("payments")[0].amount, -1000)
|
||||
self.assertEqual(pos_return.get("payments")[0].amount, -500)
|
||||
self.assertEqual(pos_return.get("payments")[1].amount, -500)
|
||||
|
||||
def test_pos_change_amount(self):
|
||||
make_pos_profile(
|
||||
@@ -1687,7 +1698,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
si.save()
|
||||
self.assertEqual(si.get("items")[0].rate, flt((price_list_rate * 25) / 100 + price_list_rate))
|
||||
|
||||
def test_outstanding_amount_after_advance_jv_cancelation(self):
|
||||
def test_outstanding_amount_after_advance_jv_cancellation(self):
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
|
||||
test_records as jv_test_records,
|
||||
)
|
||||
@@ -1731,7 +1742,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
flt(si.rounded_total + si.total_advance, si.precision("outstanding_amount")),
|
||||
)
|
||||
|
||||
def test_outstanding_amount_after_advance_payment_entry_cancelation(self):
|
||||
def test_outstanding_amount_after_advance_payment_entry_cancellation(self):
|
||||
pe = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Entry",
|
||||
@@ -2369,29 +2380,6 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
acc_settings.save()
|
||||
|
||||
def test_inter_company_transaction(self):
|
||||
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
|
||||
|
||||
create_internal_customer(
|
||||
customer_name="_Test Internal Customer",
|
||||
represents_company="_Test Company 1",
|
||||
allowed_to_interact_with="Wind Power LLC",
|
||||
)
|
||||
|
||||
if not frappe.db.exists("Supplier", "_Test Internal Supplier"):
|
||||
supplier = frappe.get_doc(
|
||||
{
|
||||
"supplier_group": "_Test Supplier Group",
|
||||
"supplier_name": "_Test Internal Supplier",
|
||||
"doctype": "Supplier",
|
||||
"is_internal_supplier": 1,
|
||||
"represents_company": "Wind Power LLC",
|
||||
}
|
||||
)
|
||||
|
||||
supplier.append("companies", {"company": "_Test Company 1"})
|
||||
|
||||
supplier.insert()
|
||||
|
||||
si = create_sales_invoice(
|
||||
company="Wind Power LLC",
|
||||
customer="_Test Internal Customer",
|
||||
@@ -2451,34 +2439,9 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
se.cancel()
|
||||
|
||||
def test_internal_transfer_gl_entry(self):
|
||||
## Create internal transfer account
|
||||
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
|
||||
|
||||
account = create_account(
|
||||
account_name="Unrealized Profit",
|
||||
parent_account="Current Liabilities - TCP1",
|
||||
company="_Test Company with perpetual inventory",
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company with perpetual inventory", "unrealized_profit_loss_account", account
|
||||
)
|
||||
|
||||
customer = create_internal_customer(
|
||||
"_Test Internal Customer 2",
|
||||
"_Test Company with perpetual inventory",
|
||||
"_Test Company with perpetual inventory",
|
||||
)
|
||||
|
||||
create_internal_supplier(
|
||||
"_Test Internal Supplier 2",
|
||||
"_Test Company with perpetual inventory",
|
||||
"_Test Company with perpetual inventory",
|
||||
)
|
||||
|
||||
si = create_sales_invoice(
|
||||
company="_Test Company with perpetual inventory",
|
||||
customer=customer,
|
||||
customer="_Test Internal Customer 2",
|
||||
debit_to="Debtors - TCP1",
|
||||
warehouse="Stores - TCP1",
|
||||
income_account="Sales - TCP1",
|
||||
@@ -2492,7 +2455,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
si.update_stock = 1
|
||||
si.items[0].target_warehouse = "Work In Progress - TCP1"
|
||||
|
||||
# Add stock to stores for succesful stock transfer
|
||||
# Add stock to stores for successful stock transfer
|
||||
make_stock_entry(
|
||||
target="Stores - TCP1", company="_Test Company with perpetual inventory", qty=1, basic_rate=100
|
||||
)
|
||||
@@ -2830,6 +2793,77 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
self.assertEqual(einvoice["ItemList"][1]["Discount"], 0)
|
||||
self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 20)
|
||||
|
||||
def test_internal_transfer_gl_precision_issues(self):
|
||||
# Make a stock queue of an item with two valuations
|
||||
|
||||
# Remove all existing stock for this
|
||||
if get_stock_balance("_Test Internal Transfer Item", "Stores - TCP1", "2022-04-10"):
|
||||
create_stock_reconciliation(
|
||||
item_code="_Test Internal Transfer Item",
|
||||
warehouse="Stores - TCP1",
|
||||
qty=0,
|
||||
rate=0,
|
||||
company="_Test Company with perpetual inventory",
|
||||
expense_account="Stock Adjustment - TCP1"
|
||||
if frappe.get_all("Stock Ledger Entry")
|
||||
else "Temporary Opening - TCP1",
|
||||
posting_date="2020-04-10",
|
||||
posting_time="14:00",
|
||||
)
|
||||
|
||||
make_stock_entry(
|
||||
item_code="_Test Internal Transfer Item",
|
||||
target="Stores - TCP1",
|
||||
qty=9000000,
|
||||
basic_rate=52.0,
|
||||
posting_date="2020-04-10",
|
||||
posting_time="14:00",
|
||||
)
|
||||
make_stock_entry(
|
||||
item_code="_Test Internal Transfer Item",
|
||||
target="Stores - TCP1",
|
||||
qty=60000000,
|
||||
basic_rate=52.349777,
|
||||
posting_date="2020-04-10",
|
||||
posting_time="14:00",
|
||||
)
|
||||
|
||||
# Make an internal transfer Sales Invoice Stock in non stock uom to check
|
||||
# for rounding errors while converting to stock uom
|
||||
si = create_sales_invoice(
|
||||
company="_Test Company with perpetual inventory",
|
||||
customer="_Test Internal Customer 2",
|
||||
item_code="_Test Internal Transfer Item",
|
||||
qty=5000000,
|
||||
uom="Box",
|
||||
debit_to="Debtors - TCP1",
|
||||
warehouse="Stores - TCP1",
|
||||
income_account="Sales - TCP1",
|
||||
expense_account="Cost of Goods Sold - TCP1",
|
||||
cost_center="Main - TCP1",
|
||||
currency="INR",
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
# Check GL Entries with precision
|
||||
si.update_stock = 1
|
||||
si.items[0].target_warehouse = "Work In Progress - TCP1"
|
||||
si.items[0].conversion_factor = 10
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
# Check if adjustment entry is created
|
||||
self.assertTrue(
|
||||
frappe.db.exists(
|
||||
"GL Entry",
|
||||
{
|
||||
"voucher_type": "Sales Invoice",
|
||||
"voucher_no": si.name,
|
||||
"remarks": "Rounding gain/loss Entry for Stock Transfer",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
def test_item_tax_net_range(self):
|
||||
item = create_item("T Shirt")
|
||||
|
||||
@@ -3278,7 +3312,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
[deferred_account, 2022.47, 0.0, "2019-03-15"],
|
||||
]
|
||||
|
||||
gl_entries = gl_entries = frappe.db.sql(
|
||||
gl_entries = frappe.db.sql(
|
||||
"""select account, debit, credit, posting_date
|
||||
from `tabGL Entry`
|
||||
where voucher_type='Journal Entry' and voucher_detail_no=%s and posting_date <= %s
|
||||
@@ -3396,6 +3430,37 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled
|
||||
)
|
||||
|
||||
def test_batch_expiry_for_sales_invoice_return(self):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item = make_item(
|
||||
"_Test Batch Item For Return Check",
|
||||
{
|
||||
"is_purchase_item": 1,
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TBIRC.#####",
|
||||
},
|
||||
)
|
||||
|
||||
pr = make_purchase_receipt(qty=1, item_code=item.name)
|
||||
|
||||
batch_no = pr.items[0].batch_no
|
||||
si = create_sales_invoice(qty=1, item_code=item.name, update_stock=1, batch_no=batch_no)
|
||||
|
||||
si.load_from_db()
|
||||
batch_no = si.items[0].batch_no
|
||||
self.assertTrue(batch_no)
|
||||
|
||||
frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1))
|
||||
|
||||
return_si = make_return_doc(si.doctype, si.name)
|
||||
return_si.save().submit()
|
||||
|
||||
self.assertTrue(return_si.docstatus == 1)
|
||||
|
||||
|
||||
def get_sales_invoice_for_e_invoice():
|
||||
si = make_sales_invoice_for_ewaybill()
|
||||
@@ -3652,6 +3717,7 @@ def create_sales_invoice(**args):
|
||||
"description": args.description or "_Test Item",
|
||||
"gst_hsn_code": "999800",
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"target_warehouse": args.target_warehouse,
|
||||
"qty": args.qty or 1,
|
||||
"uom": args.uom or "Nos",
|
||||
"stock_uom": args.uom or "Nos",
|
||||
@@ -3664,8 +3730,9 @@ def create_sales_invoice(**args):
|
||||
"discount_amount": args.discount_amount or 0,
|
||||
"cost_center": args.cost_center or "_Test Cost Center - _TC",
|
||||
"serial_no": args.serial_no,
|
||||
"conversion_factor": 1,
|
||||
"conversion_factor": args.get("conversion_factor", 1),
|
||||
"incoming_rate": args.incoming_rate or 0,
|
||||
"batch_no": args.batch_no or None,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -3777,6 +3844,34 @@ def get_taxes_and_charges():
|
||||
]
|
||||
|
||||
|
||||
def create_internal_parties():
|
||||
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
|
||||
|
||||
create_internal_customer(
|
||||
customer_name="_Test Internal Customer",
|
||||
represents_company="_Test Company 1",
|
||||
allowed_to_interact_with="Wind Power LLC",
|
||||
)
|
||||
|
||||
create_internal_customer(
|
||||
customer_name="_Test Internal Customer 2",
|
||||
represents_company="_Test Company with perpetual inventory",
|
||||
allowed_to_interact_with="_Test Company with perpetual inventory",
|
||||
)
|
||||
|
||||
create_internal_supplier(
|
||||
supplier_name="_Test Internal Supplier",
|
||||
represents_company="Wind Power LLC",
|
||||
allowed_to_interact_with="_Test Company 1",
|
||||
)
|
||||
|
||||
create_internal_supplier(
|
||||
supplier_name="_Test Internal Supplier 2",
|
||||
represents_company="_Test Company with perpetual inventory",
|
||||
allowed_to_interact_with="_Test Company with perpetual inventory",
|
||||
)
|
||||
|
||||
|
||||
def create_internal_supplier(supplier_name, represents_company, allowed_to_interact_with):
|
||||
if not frappe.db.exists("Supplier", supplier_name):
|
||||
supplier = frappe.get_doc(
|
||||
@@ -3799,6 +3894,19 @@ def create_internal_supplier(supplier_name, represents_company, allowed_to_inter
|
||||
return supplier_name
|
||||
|
||||
|
||||
def setup_accounts():
|
||||
## Create internal transfer account
|
||||
account = create_account(
|
||||
account_name="Unrealized Profit",
|
||||
parent_account="Current Liabilities - TCP1",
|
||||
company="_Test Company with perpetual inventory",
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company with perpetual inventory", "unrealized_profit_loss_account", account
|
||||
)
|
||||
|
||||
|
||||
def add_taxes(doc):
|
||||
doc.append(
|
||||
"taxes",
|
||||
|
||||
@@ -812,6 +812,8 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Incoming Rate (Costing)",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"precision": "6",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
@@ -841,7 +843,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-08-26 12:06:31.205417",
|
||||
"modified": "2022-10-10 20:57:38.340026",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "autoincrement",
|
||||
"creation": "2022-09-13 16:18:59.404842",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"voucher_type",
|
||||
"voucher_name",
|
||||
"taxable_amount"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Voucher Type",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Voucher Name",
|
||||
"options": "voucher_type"
|
||||
},
|
||||
{
|
||||
"fieldname": "taxable_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Taxable Amount",
|
||||
"options": "Company:company:default_currency"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-09-13 23:40:41.479208",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withheld Vouchers",
|
||||
"naming_rule": "Autoincrement",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class TaxWithheldVouchers(Document):
|
||||
pass
|
||||
@@ -100,7 +100,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
||||
).format(tax_withholding_category, inv.company, party)
|
||||
)
|
||||
|
||||
tax_amount, tax_deducted, tax_deducted_on_advances = get_tax_amount(
|
||||
tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount = get_tax_amount(
|
||||
party_type, parties, inv, tax_details, posting_date, pan_no
|
||||
)
|
||||
|
||||
@@ -110,7 +110,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
||||
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
|
||||
|
||||
if inv.doctype == "Purchase Invoice":
|
||||
return tax_row, tax_deducted_on_advances
|
||||
return tax_row, tax_deducted_on_advances, voucher_wise_amount
|
||||
else:
|
||||
return tax_row
|
||||
|
||||
@@ -208,7 +208,9 @@ def get_lower_deduction_certificate(tax_details, pan_no):
|
||||
|
||||
|
||||
def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=None):
|
||||
vouchers = get_invoice_vouchers(parties, tax_details, inv.company, party_type=party_type)
|
||||
vouchers, voucher_wise_amount = get_invoice_vouchers(
|
||||
parties, tax_details, inv.company, party_type=party_type
|
||||
)
|
||||
advance_vouchers = get_advance_vouchers(
|
||||
parties,
|
||||
company=inv.company,
|
||||
@@ -227,6 +229,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
tax_deducted = get_deducted_tax(taxable_vouchers, tax_details)
|
||||
|
||||
tax_amount = 0
|
||||
|
||||
if party_type == "Supplier":
|
||||
ldc = get_lower_deduction_certificate(tax_details, pan_no)
|
||||
if tax_deducted:
|
||||
@@ -237,6 +240,9 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
)
|
||||
else:
|
||||
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
|
||||
|
||||
# once tds is deducted, not need to add vouchers in the invoice
|
||||
voucher_wise_amount = {}
|
||||
else:
|
||||
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers)
|
||||
|
||||
@@ -252,12 +258,13 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
if cint(tax_details.round_off_tax_amount):
|
||||
tax_amount = round(tax_amount)
|
||||
|
||||
return tax_amount, tax_deducted, tax_deducted_on_advances
|
||||
return tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount
|
||||
|
||||
|
||||
def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
dr_or_cr = "credit" if party_type == "Supplier" else "debit"
|
||||
doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice"
|
||||
voucher_wise_amount = {}
|
||||
vouchers = []
|
||||
|
||||
filters = {
|
||||
"company": company,
|
||||
@@ -272,29 +279,40 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
{"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")}
|
||||
)
|
||||
|
||||
invoices = frappe.get_all(doctype, filters=filters, pluck="name") or [""]
|
||||
invoices_details = frappe.get_all(doctype, filters=filters, fields=["name", "base_net_total"])
|
||||
|
||||
journal_entries = frappe.db.sql(
|
||||
for d in invoices_details:
|
||||
vouchers.append(d.name)
|
||||
voucher_wise_amount.update({d.name: {"amount": d.base_net_total, "voucher_type": doctype}})
|
||||
|
||||
journal_entries_details = frappe.db.sql(
|
||||
"""
|
||||
SELECT j.name
|
||||
SELECT j.name, ja.credit - ja.debit AS amount
|
||||
FROM `tabJournal Entry` j, `tabJournal Entry Account` ja
|
||||
WHERE
|
||||
j.docstatus = 1
|
||||
j.name = ja.parent
|
||||
AND j.docstatus = 1
|
||||
AND j.is_opening = 'No'
|
||||
AND j.posting_date between %s and %s
|
||||
AND ja.{dr_or_cr} > 0
|
||||
AND ja.party in %s
|
||||
""".format(
|
||||
dr_or_cr=dr_or_cr
|
||||
AND j.apply_tds = 1
|
||||
AND j.tax_withholding_category = %s
|
||||
""",
|
||||
(
|
||||
tax_details.from_date,
|
||||
tax_details.to_date,
|
||||
tuple(parties),
|
||||
tax_details.get("tax_withholding_category"),
|
||||
),
|
||||
(tax_details.from_date, tax_details.to_date, tuple(parties)),
|
||||
as_list=1,
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if journal_entries:
|
||||
journal_entries = journal_entries[0]
|
||||
if journal_entries_details:
|
||||
for d in journal_entries_details:
|
||||
vouchers.append(d.name)
|
||||
voucher_wise_amount.update({d.name: {"amount": d.amount, "voucher_type": "Journal Entry"}})
|
||||
|
||||
return invoices + journal_entries
|
||||
return vouchers, voucher_wise_amount
|
||||
|
||||
|
||||
def get_advance_vouchers(
|
||||
@@ -311,6 +329,9 @@ def get_advance_vouchers(
|
||||
"party": ["in", parties],
|
||||
}
|
||||
|
||||
if party_type == "Customer":
|
||||
filters.update({"against_voucher": ["is", "not set"]})
|
||||
|
||||
if company:
|
||||
filters["company"] = company
|
||||
if from_date and to_date:
|
||||
@@ -320,23 +341,25 @@ def get_advance_vouchers(
|
||||
|
||||
|
||||
def get_taxes_deducted_on_advances_allocated(inv, tax_details):
|
||||
advances = [d.reference_name for d in inv.get("advances")]
|
||||
tax_info = []
|
||||
|
||||
if advances:
|
||||
pe = frappe.qb.DocType("Payment Entry").as_("pe")
|
||||
at = frappe.qb.DocType("Advance Taxes and Charges").as_("at")
|
||||
if inv.get("advances"):
|
||||
advances = [d.reference_name for d in inv.get("advances")]
|
||||
|
||||
tax_info = (
|
||||
frappe.qb.from_(at)
|
||||
.inner_join(pe)
|
||||
.on(pe.name == at.parent)
|
||||
.select(at.parent, at.name, at.tax_amount, at.allocated_amount)
|
||||
.where(pe.tax_withholding_category == tax_details.get("tax_withholding_category"))
|
||||
.where(at.parent.isin(advances))
|
||||
.where(at.account_head == tax_details.account_head)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
if advances:
|
||||
pe = frappe.qb.DocType("Payment Entry").as_("pe")
|
||||
at = frappe.qb.DocType("Advance Taxes and Charges").as_("at")
|
||||
|
||||
tax_info = (
|
||||
frappe.qb.from_(at)
|
||||
.inner_join(pe)
|
||||
.on(pe.name == at.parent)
|
||||
.select(at.parent, at.name, at.tax_amount, at.allocated_amount)
|
||||
.where(pe.tax_withholding_category == tax_details.get("tax_withholding_category"))
|
||||
.where(at.parent.isin(advances))
|
||||
.where(at.account_head == tax_details.account_head)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
return tax_info
|
||||
|
||||
@@ -358,6 +381,9 @@ def get_deducted_tax(taxable_vouchers, tax_details):
|
||||
|
||||
def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
|
||||
tds_amount = 0
|
||||
supp_credit_amt = 0.0
|
||||
supp_jv_credit_amt = 0.0
|
||||
|
||||
invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1}
|
||||
|
||||
field = "sum(net_total)"
|
||||
@@ -366,30 +392,25 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
|
||||
invoice_filters.pop("apply_tds", None)
|
||||
field = "sum(grand_total)"
|
||||
|
||||
supp_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0
|
||||
if vouchers:
|
||||
supp_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0
|
||||
|
||||
supp_jv_credit_amt = (
|
||||
frappe.db.get_value(
|
||||
"Journal Entry Account",
|
||||
{
|
||||
"parent": ("in", vouchers),
|
||||
"docstatus": 1,
|
||||
"party": ("in", parties),
|
||||
"reference_type": ("!=", "Purchase Invoice"),
|
||||
},
|
||||
"sum(credit_in_account_currency)",
|
||||
)
|
||||
or 0.0
|
||||
)
|
||||
supp_jv_credit_amt = (
|
||||
frappe.db.get_value(
|
||||
"Journal Entry Account",
|
||||
{
|
||||
"parent": ("in", vouchers),
|
||||
"docstatus": 1,
|
||||
"party": ("in", parties),
|
||||
"reference_type": ("!=", "Purchase Invoice"),
|
||||
},
|
||||
"sum(credit_in_account_currency)",
|
||||
)
|
||||
) or 0.0
|
||||
|
||||
supp_credit_amt += supp_jv_credit_amt
|
||||
supp_credit_amt += inv.net_total
|
||||
|
||||
debit_note_amount = get_debit_note_amount(
|
||||
parties, tax_details.from_date, tax_details.to_date, inv.company
|
||||
)
|
||||
supp_credit_amt -= debit_note_amount
|
||||
|
||||
threshold = tax_details.get("threshold", 0)
|
||||
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
|
||||
|
||||
@@ -401,7 +422,10 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
|
||||
):
|
||||
# Get net total again as TDS is calculated on net total
|
||||
# Grand is used to just check for threshold breach
|
||||
net_total = frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(net_total)") or 0.0
|
||||
net_total = 0
|
||||
if vouchers:
|
||||
net_total = frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(net_total)")
|
||||
|
||||
net_total += inv.net_total
|
||||
supp_credit_amt = net_total - cumulative_threshold
|
||||
|
||||
@@ -422,36 +446,40 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
|
||||
|
||||
def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
||||
tcs_amount = 0
|
||||
invoiced_amt = 0
|
||||
advance_amt = 0
|
||||
|
||||
# sum of debit entries made from sales invoices
|
||||
invoiced_amt = (
|
||||
frappe.db.get_value(
|
||||
"GL Entry",
|
||||
{
|
||||
"is_cancelled": 0,
|
||||
"party": ["in", parties],
|
||||
"company": inv.company,
|
||||
"voucher_no": ["in", vouchers],
|
||||
},
|
||||
"sum(debit)",
|
||||
if vouchers:
|
||||
invoiced_amt = (
|
||||
frappe.db.get_value(
|
||||
"GL Entry",
|
||||
{
|
||||
"is_cancelled": 0,
|
||||
"party": ["in", parties],
|
||||
"company": inv.company,
|
||||
"voucher_no": ["in", vouchers],
|
||||
},
|
||||
"sum(debit)",
|
||||
)
|
||||
or 0.0
|
||||
)
|
||||
or 0.0
|
||||
)
|
||||
|
||||
# sum of credit entries made from PE / JV with unset 'against voucher'
|
||||
advance_amt = (
|
||||
frappe.db.get_value(
|
||||
"GL Entry",
|
||||
{
|
||||
"is_cancelled": 0,
|
||||
"party": ["in", parties],
|
||||
"company": inv.company,
|
||||
"voucher_no": ["in", adv_vouchers],
|
||||
},
|
||||
"sum(credit)",
|
||||
if advance_amt:
|
||||
advance_amt = (
|
||||
frappe.db.get_value(
|
||||
"GL Entry",
|
||||
{
|
||||
"is_cancelled": 0,
|
||||
"party": ["in", parties],
|
||||
"company": inv.company,
|
||||
"voucher_no": ["in", adv_vouchers],
|
||||
},
|
||||
"sum(credit)",
|
||||
)
|
||||
or 0.0
|
||||
)
|
||||
or 0.0
|
||||
)
|
||||
|
||||
# sum of credit entries made from sales invoice
|
||||
credit_note_amt = sum(
|
||||
@@ -506,22 +534,6 @@ def get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net
|
||||
return tds_amount
|
||||
|
||||
|
||||
def get_debit_note_amount(suppliers, from_date, to_date, company=None):
|
||||
|
||||
filters = {
|
||||
"supplier": ["in", suppliers],
|
||||
"is_return": 1,
|
||||
"docstatus": 1,
|
||||
"posting_date": ["between", (from_date, to_date)],
|
||||
}
|
||||
fields = ["abs(sum(net_total)) as net_total"]
|
||||
|
||||
if company:
|
||||
filters["company"] = company
|
||||
|
||||
return frappe.get_all("Purchase Invoice", filters, fields)[0].get("net_total") or 0.0
|
||||
|
||||
|
||||
def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details):
|
||||
if current_amount < (certificate_limit - deducted_amount):
|
||||
return current_amount * rate / 100
|
||||
|
||||
@@ -52,7 +52,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
invoices.append(pi)
|
||||
|
||||
# delete invoices to avoid clashing
|
||||
for d in invoices:
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_single_threshold_tds(self):
|
||||
@@ -88,7 +88,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
self.assertEqual(pi.taxes_and_charges_deducted, 1000)
|
||||
|
||||
# delete invoices to avoid clashing
|
||||
for d in invoices:
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_tax_withholding_category_checks(self):
|
||||
@@ -114,7 +114,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
# TDS should be applied only on 1000
|
||||
self.assertEqual(pi1.taxes[0].tax_amount, 1000)
|
||||
|
||||
for d in invoices:
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_cumulative_threshold_tcs(self):
|
||||
@@ -148,8 +148,8 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
self.assertEqual(tcs_charged, 500)
|
||||
invoices.append(si)
|
||||
|
||||
# delete invoices to avoid clashing
|
||||
for d in invoices:
|
||||
# cancel invoices to avoid clashing
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_tds_calculation_on_net_total(self):
|
||||
@@ -182,8 +182,8 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
|
||||
self.assertEqual(pi1.taxes[0].tax_amount, 4000)
|
||||
|
||||
# delete invoices to avoid clashing
|
||||
for d in invoices:
|
||||
# cancel invoices to avoid clashing
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_multi_category_single_supplier(self):
|
||||
@@ -207,8 +207,50 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
|
||||
self.assertEqual(pi1.taxes[0].tax_amount, 250)
|
||||
|
||||
# delete invoices to avoid clashing
|
||||
for d in invoices:
|
||||
# cancel invoices to avoid clashing
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_tax_withholding_category_voucher_display(self):
|
||||
frappe.db.set_value(
|
||||
"Supplier", "Test TDS Supplier6", "tax_withholding_category", "Test Multi Invoice Category"
|
||||
)
|
||||
invoices = []
|
||||
|
||||
pi = create_purchase_invoice(supplier="Test TDS Supplier6", rate=4000, do_not_save=True)
|
||||
pi.apply_tds = 1
|
||||
pi.tax_withholding_category = "Test Multi Invoice Category"
|
||||
pi.save()
|
||||
pi.submit()
|
||||
invoices.append(pi)
|
||||
|
||||
pi1 = create_purchase_invoice(supplier="Test TDS Supplier6", rate=2000, do_not_save=True)
|
||||
pi1.apply_tds = 1
|
||||
pi1.is_return = 1
|
||||
pi1.items[0].qty = -1
|
||||
pi1.tax_withholding_category = "Test Multi Invoice Category"
|
||||
pi1.save()
|
||||
pi1.submit()
|
||||
invoices.append(pi1)
|
||||
|
||||
pi2 = create_purchase_invoice(supplier="Test TDS Supplier6", rate=9000, do_not_save=True)
|
||||
pi2.apply_tds = 1
|
||||
pi2.tax_withholding_category = "Test Multi Invoice Category"
|
||||
pi2.save()
|
||||
pi2.submit()
|
||||
invoices.append(pi2)
|
||||
|
||||
pi2.load_from_db()
|
||||
|
||||
self.assertTrue(pi2.taxes[0].tax_amount, 1100)
|
||||
|
||||
self.assertTrue(pi2.tax_withheld_vouchers[0].voucher_name == pi1.name)
|
||||
self.assertTrue(pi2.tax_withheld_vouchers[0].taxable_amount == pi1.net_total)
|
||||
self.assertTrue(pi2.tax_withheld_vouchers[1].voucher_name == pi.name)
|
||||
self.assertTrue(pi2.tax_withheld_vouchers[1].taxable_amount == pi.net_total)
|
||||
|
||||
# cancel invoices to avoid clashing
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
|
||||
@@ -308,6 +350,7 @@ def create_records():
|
||||
"Test TDS Supplier3",
|
||||
"Test TDS Supplier4",
|
||||
"Test TDS Supplier5",
|
||||
"Test TDS Supplier6",
|
||||
]:
|
||||
if frappe.db.exists("Supplier", name):
|
||||
continue
|
||||
@@ -498,3 +541,22 @@ def create_tax_with_holding_category():
|
||||
"accounts": [{"company": "_Test Company", "account": "TDS - _TC"}],
|
||||
}
|
||||
).insert()
|
||||
|
||||
if not frappe.db.exists("Tax Withholding Category", "Test Multi Invoice Category"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Tax Withholding Category",
|
||||
"name": "Test Multi Invoice Category",
|
||||
"category_name": "Test Multi Invoice Category",
|
||||
"rates": [
|
||||
{
|
||||
"from_date": fiscal_year[1],
|
||||
"to_date": fiscal_year[2],
|
||||
"tax_withholding_rate": 10,
|
||||
"single_threshold": 5000,
|
||||
"cumulative_threshold": 10000,
|
||||
}
|
||||
],
|
||||
"accounts": [{"company": "_Test Company", "account": "TDS - _TC"}],
|
||||
}
|
||||
).insert()
|
||||
|
||||
@@ -50,6 +50,10 @@
|
||||
<div class="col-xs-4"><label>Document No</label></div>
|
||||
<div class="col-xs-8 value">{{ einvoice.DocDtls.No }}</div>
|
||||
</div>
|
||||
<div class="row data-field">
|
||||
<div class="col-xs-4"><label>Document Date</label></div>
|
||||
<div class="col-xs-8 value">{{ einvoice.DocDtls.Dt }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-4 column-break">
|
||||
<img src="{{ doc.qrcode_image }}" width="175px" style="float: right;">
|
||||
|
||||
@@ -280,9 +280,9 @@ def get_conditions(filters):
|
||||
or filters.get("party")
|
||||
or filters.get("group_by") in ["Group by Account", "Group by Party"]
|
||||
):
|
||||
conditions.append("posting_date >=%(from_date)s")
|
||||
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
|
||||
|
||||
conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')")
|
||||
conditions.append("(posting_date <=%(to_date)s)")
|
||||
|
||||
if filters.get("project"):
|
||||
conditions.append("project in %(project)s")
|
||||
|
||||
@@ -155,7 +155,6 @@ def adjust_account(data, period_list, consolidated=False):
|
||||
for d in data:
|
||||
for period in period_list:
|
||||
key = period if consolidated else period.key
|
||||
d[key] = totals[d["account"]]
|
||||
d["total"] = totals[d["account"]]
|
||||
return data
|
||||
|
||||
|
||||
@@ -19,14 +19,19 @@ def execute(filters=None):
|
||||
return _execute(filters)
|
||||
|
||||
|
||||
def _execute(filters=None, additional_table_columns=None, additional_query_columns=None):
|
||||
def _execute(
|
||||
filters=None,
|
||||
additional_table_columns=None,
|
||||
additional_query_columns=None,
|
||||
additional_conditions=None,
|
||||
):
|
||||
if not filters:
|
||||
filters = {}
|
||||
columns = get_columns(additional_table_columns, filters)
|
||||
|
||||
company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
|
||||
|
||||
item_list = get_items(filters, additional_query_columns)
|
||||
item_list = get_items(filters, additional_query_columns, additional_conditions)
|
||||
if item_list:
|
||||
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
|
||||
|
||||
@@ -97,6 +102,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
|
||||
row.update({"rate": d.base_net_rate, "amount": d.base_net_amount})
|
||||
|
||||
total_tax = 0
|
||||
total_other_charges = 0
|
||||
for tax in tax_columns:
|
||||
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
|
||||
row.update(
|
||||
@@ -105,10 +111,18 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
|
||||
frappe.scrub(tax + " Amount"): item_tax.get("tax_amount", 0),
|
||||
}
|
||||
)
|
||||
total_tax += flt(item_tax.get("tax_amount"))
|
||||
if item_tax.get("is_other_charges"):
|
||||
total_other_charges += flt(item_tax.get("tax_amount"))
|
||||
else:
|
||||
total_tax += flt(item_tax.get("tax_amount"))
|
||||
|
||||
row.update(
|
||||
{"total_tax": total_tax, "total": d.base_net_amount + total_tax, "currency": company_currency}
|
||||
{
|
||||
"total_tax": total_tax,
|
||||
"total_other_charges": total_other_charges,
|
||||
"total": d.base_net_amount + total_tax,
|
||||
"currency": company_currency,
|
||||
}
|
||||
)
|
||||
|
||||
if filters.get("group_by"):
|
||||
@@ -319,7 +333,7 @@ def get_columns(additional_table_columns, filters):
|
||||
return columns
|
||||
|
||||
|
||||
def get_conditions(filters):
|
||||
def get_conditions(filters, additional_conditions=None):
|
||||
conditions = ""
|
||||
|
||||
for opts in (
|
||||
@@ -332,6 +346,9 @@ def get_conditions(filters):
|
||||
if filters.get(opts[0]):
|
||||
conditions += opts[1]
|
||||
|
||||
if additional_conditions:
|
||||
conditions += additional_conditions
|
||||
|
||||
if filters.get("mode_of_payment"):
|
||||
conditions += """ and exists(select name from `tabSales Invoice Payment`
|
||||
where parent=`tabSales Invoice`.name
|
||||
@@ -367,8 +384,8 @@ def get_group_by_conditions(filters, doctype):
|
||||
return "ORDER BY `tab{0}`.{1}".format(doctype, frappe.scrub(filters.get("group_by")))
|
||||
|
||||
|
||||
def get_items(filters, additional_query_columns):
|
||||
conditions = get_conditions(filters)
|
||||
def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
conditions = get_conditions(filters, additional_conditions)
|
||||
|
||||
if additional_query_columns:
|
||||
additional_query_columns = ", " + ", ".join(additional_query_columns)
|
||||
@@ -477,7 +494,7 @@ def get_tax_accounts(
|
||||
tax_details = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
name, parent, description, item_wise_tax_detail,
|
||||
name, parent, description, item_wise_tax_detail, account_head,
|
||||
charge_type, {add_deduct_tax}, base_tax_amount_after_discount_amount
|
||||
from `tab%s`
|
||||
where
|
||||
@@ -493,11 +510,22 @@ def get_tax_accounts(
|
||||
tuple([doctype] + list(invoice_item_row)),
|
||||
)
|
||||
|
||||
account_doctype = frappe.qb.DocType("Account")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(account_doctype)
|
||||
.select(account_doctype.name)
|
||||
.where((account_doctype.account_type == "Tax"))
|
||||
)
|
||||
|
||||
tax_accounts = query.run()
|
||||
|
||||
for (
|
||||
name,
|
||||
parent,
|
||||
description,
|
||||
item_wise_tax_detail,
|
||||
account_head,
|
||||
charge_type,
|
||||
add_deduct_tax,
|
||||
tax_amount,
|
||||
@@ -540,7 +568,11 @@ def get_tax_accounts(
|
||||
)
|
||||
|
||||
itemised_tax.setdefault(d.name, {})[description] = frappe._dict(
|
||||
{"tax_rate": tax_rate, "tax_amount": tax_value}
|
||||
{
|
||||
"tax_rate": tax_rate,
|
||||
"tax_amount": tax_value,
|
||||
"is_other_charges": 0 if tuple([account_head]) in tax_accounts else 1,
|
||||
}
|
||||
)
|
||||
|
||||
except ValueError:
|
||||
@@ -583,6 +615,13 @@ def get_tax_accounts(
|
||||
"options": "currency",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Total Other Charges"),
|
||||
"fieldname": "total_other_charges",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Total"),
|
||||
"fieldname": "total",
|
||||
|
||||
@@ -370,7 +370,7 @@ def get_conditions(filters):
|
||||
where parent=`tabSales Invoice`.name
|
||||
and ifnull(`tab{table}`.{field}, '') = %({field})s)"""
|
||||
|
||||
conditions += get_sales_invoice_item_field_condition("mode_of_payments", "Sales Invoice Payment")
|
||||
conditions += get_sales_invoice_item_field_condition("mode_of_payment", "Sales Invoice Payment")
|
||||
conditions += get_sales_invoice_item_field_condition("cost_center")
|
||||
conditions += get_sales_invoice_item_field_condition("warehouse")
|
||||
conditions += get_sales_invoice_item_field_condition("brand")
|
||||
|
||||
@@ -172,6 +172,7 @@ def get_rootwise_opening_balances(filters, report_type):
|
||||
query_filters = {
|
||||
"company": filters.company,
|
||||
"from_date": filters.from_date,
|
||||
"to_date": filters.to_date,
|
||||
"report_type": report_type,
|
||||
"year_start_date": filters.year_start_date,
|
||||
"project": filters.project,
|
||||
@@ -200,7 +201,7 @@ def get_rootwise_opening_balances(filters, report_type):
|
||||
where
|
||||
company=%(company)s
|
||||
{additional_conditions}
|
||||
and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes')
|
||||
and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s))
|
||||
and account in (select name from `tabAccount` where report_type=%(report_type)s)
|
||||
and is_cancelled = 0
|
||||
group by account""".format(
|
||||
|
||||
@@ -106,12 +106,17 @@ def get_opening_balances(filters):
|
||||
where company=%(company)s
|
||||
and is_cancelled=0
|
||||
and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != ''
|
||||
and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes')
|
||||
and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s))
|
||||
{account_filter}
|
||||
group by party""".format(
|
||||
account_filter=account_filter
|
||||
),
|
||||
{"company": filters.company, "from_date": filters.from_date, "party_type": filters.party_type},
|
||||
{
|
||||
"company": filters.company,
|
||||
"from_date": filters.from_date,
|
||||
"to_date": filters.to_date,
|
||||
"party_type": filters.party_type,
|
||||
},
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -835,10 +835,7 @@ def remove_return_pos_invoices(party_type, party, invoice_list):
|
||||
else:
|
||||
return invoice_list
|
||||
|
||||
# remove pos return invoices from invoice_list
|
||||
for idx, inv in enumerate(invoice_list, 0):
|
||||
if inv.voucher_no in return_pos:
|
||||
del invoice_list[idx]
|
||||
invoice_list = [x for x in invoice_list if x.voucher_no not in return_pos]
|
||||
|
||||
return invoice_list
|
||||
|
||||
|
||||
@@ -384,7 +384,11 @@ frappe.ui.form.on('Asset', {
|
||||
|
||||
set_values_from_purchase_doc: function(frm, doctype, purchase_doc) {
|
||||
frm.set_value('company', purchase_doc.company);
|
||||
frm.set_value('purchase_date', purchase_doc.posting_date);
|
||||
if (purchase_doc.bill_date) {
|
||||
frm.set_value('purchase_date', purchase_doc.bill_date);
|
||||
} else {
|
||||
frm.set_value('purchase_date', purchase_doc.posting_date);
|
||||
}
|
||||
const item = purchase_doc.items.find(item => item.item_code === frm.doc.item_code);
|
||||
if (!item) {
|
||||
doctype_field = frappe.scrub(doctype)
|
||||
|
||||
@@ -819,7 +819,9 @@ class Asset(AccountsController):
|
||||
|
||||
|
||||
def update_maintenance_status():
|
||||
assets = frappe.get_all("Asset", filters={"docstatus": 1, "maintenance_required": 1})
|
||||
assets = frappe.get_all(
|
||||
"Asset", filters={"docstatus": 1, "maintenance_required": 1, "disposal_date": ("is", "not set")}
|
||||
)
|
||||
|
||||
for asset in assets:
|
||||
asset = frappe.get_doc("Asset", asset.name)
|
||||
|
||||
@@ -7,7 +7,7 @@ import frappe
|
||||
from frappe.utils import add_days, add_months, cstr, flt, get_last_day, getdate, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.assets.doctype.asset.asset import make_sales_invoice
|
||||
from erpnext.assets.doctype.asset.asset import make_sales_invoice, update_maintenance_status
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
post_depreciation_entries,
|
||||
restore_asset,
|
||||
@@ -238,6 +238,34 @@ class TestAsset(AssetSetup):
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
|
||||
|
||||
def test_asset_with_maintenance_required_status_after_sale(self):
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2020-06-06",
|
||||
purchase_date="2020-01-01",
|
||||
expected_value_after_useful_life=10000,
|
||||
total_number_of_depreciations=3,
|
||||
frequency_of_depreciation=10,
|
||||
maintenance_required=1,
|
||||
depreciation_start_date="2020-12-31",
|
||||
submit=1,
|
||||
)
|
||||
|
||||
post_depreciation_entries(date="2021-01-01")
|
||||
|
||||
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
|
||||
si.customer = "_Test Customer"
|
||||
si.due_date = nowdate()
|
||||
si.get("items")[0].rate = 25000
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
|
||||
|
||||
update_maintenance_status()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
|
||||
|
||||
def test_expense_head(self):
|
||||
pr = make_purchase_receipt(
|
||||
item_code="Macbook Pro", qty=2, rate=200000.0, location="Test Location"
|
||||
@@ -1353,6 +1381,7 @@ def create_asset(**args):
|
||||
"number_of_depreciations_booked": args.number_of_depreciations_booked or 0,
|
||||
"gross_purchase_amount": args.gross_purchase_amount or 100000,
|
||||
"purchase_receipt_amount": args.purchase_receipt_amount or 100000,
|
||||
"maintenance_required": args.maintenance_required or 0,
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"available_for_use_date": args.available_for_use_date or "2020-06-06",
|
||||
"location": args.location or "Test Location",
|
||||
|
||||
@@ -43,6 +43,11 @@ frappe.ui.form.on("Purchase Order", {
|
||||
erpnext.queries.setup_queries(frm, "Warehouse", function() {
|
||||
return erpnext.queries.warehouse(frm.doc);
|
||||
});
|
||||
|
||||
// On cancel and amending a purchase order with advance payment, reset advance paid amount
|
||||
if (frm.is_new()) {
|
||||
frm.set_value("advance_paid", 0)
|
||||
}
|
||||
},
|
||||
|
||||
apply_tds: function(frm) {
|
||||
|
||||
@@ -439,7 +439,6 @@
|
||||
"fieldname": "ignore_pricing_rule",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Pricing Rule",
|
||||
"no_copy": 1,
|
||||
"permlevel": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
@@ -1170,7 +1169,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-26 12:16:38.694276",
|
||||
"modified": "2022-09-16 17:45:04.954055",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
|
||||
@@ -18,7 +18,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
||||
get_party_tax_withholding_details,
|
||||
)
|
||||
from erpnext.accounts.party import get_party_account_currency
|
||||
from erpnext.accounts.party import get_party_account, get_party_account_currency
|
||||
from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items
|
||||
from erpnext.controllers.buying_controller import BuyingController
|
||||
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
||||
@@ -323,6 +323,7 @@ class PurchaseOrder(BuyingController):
|
||||
update_linked_doc(self.doctype, self.name, self.inter_company_order_reference)
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Payment Ledger Entry")
|
||||
super(PurchaseOrder, self).on_cancel()
|
||||
|
||||
if self.is_against_so():
|
||||
@@ -532,6 +533,7 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
||||
target.set_advances()
|
||||
|
||||
target.set_payment_schedule()
|
||||
target.credit_to = get_party_account("Supplier", source.supplier, source.company)
|
||||
|
||||
def update_item(obj, target, source_parent):
|
||||
target.amount = flt(obj.amount) - flt(obj.billed_amt)
|
||||
|
||||
@@ -152,6 +152,7 @@ class AccountsController(TransactionBase):
|
||||
self.validate_inter_company_reference()
|
||||
|
||||
self.disable_pricing_rule_on_internal_transfer()
|
||||
self.disable_tax_included_prices_for_internal_transfer()
|
||||
self.set_incoming_rate()
|
||||
|
||||
if self.meta.get_field("currency"):
|
||||
@@ -395,6 +396,20 @@ class AccountsController(TransactionBase):
|
||||
alert=1,
|
||||
)
|
||||
|
||||
def disable_tax_included_prices_for_internal_transfer(self):
|
||||
if self.is_internal_transfer():
|
||||
tax_updated = False
|
||||
for tax in self.get("taxes"):
|
||||
if tax.get("included_in_print_rate"):
|
||||
tax.included_in_print_rate = 0
|
||||
tax_updated = True
|
||||
|
||||
if tax_updated:
|
||||
frappe.msgprint(
|
||||
_("Disabled tax included prices since this {} is an internal transfer").format(self.doctype),
|
||||
alert=1,
|
||||
)
|
||||
|
||||
def validate_due_date(self):
|
||||
if self.get("is_pos"):
|
||||
return
|
||||
|
||||
@@ -193,16 +193,16 @@ class BuyingController(StockController, Subcontracting):
|
||||
|
||||
if self.meta.get_field("base_in_words"):
|
||||
if self.meta.get_field("base_rounded_total") and not self.is_rounded_total_disabled():
|
||||
amount = self.base_rounded_total
|
||||
amount = abs(self.base_rounded_total)
|
||||
else:
|
||||
amount = self.base_grand_total
|
||||
amount = abs(self.base_grand_total)
|
||||
self.base_in_words = money_in_words(amount, self.company_currency)
|
||||
|
||||
if self.meta.get_field("in_words"):
|
||||
if self.meta.get_field("rounded_total") and not self.is_rounded_total_disabled():
|
||||
amount = self.rounded_total
|
||||
amount = abs(self.rounded_total)
|
||||
else:
|
||||
amount = self.grand_total
|
||||
amount = abs(self.grand_total)
|
||||
|
||||
self.in_words = money_in_words(amount, self.currency)
|
||||
|
||||
|
||||
@@ -439,11 +439,17 @@ class SellingController(StockController):
|
||||
# For internal transfers use incoming rate as the valuation rate
|
||||
if self.is_internal_transfer():
|
||||
if d.doctype == "Packed Item":
|
||||
incoming_rate = flt(d.incoming_rate * d.conversion_factor, d.precision("incoming_rate"))
|
||||
incoming_rate = flt(
|
||||
flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor,
|
||||
d.precision("incoming_rate"),
|
||||
)
|
||||
if d.incoming_rate != incoming_rate:
|
||||
d.incoming_rate = incoming_rate
|
||||
else:
|
||||
rate = flt(d.incoming_rate * d.conversion_factor, d.precision("rate"))
|
||||
rate = flt(
|
||||
flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor,
|
||||
d.precision("rate"),
|
||||
)
|
||||
if d.rate != rate:
|
||||
d.rate = rate
|
||||
frappe.msgprint(
|
||||
|
||||
@@ -139,13 +139,15 @@ class StockController(AccountsController):
|
||||
warehouse_with_no_account = []
|
||||
precision = self.get_debit_field_precision()
|
||||
for item_row in voucher_details:
|
||||
|
||||
sle_list = sle_map.get(item_row.name)
|
||||
sle_rounding_diff = 0.0
|
||||
if sle_list:
|
||||
for sle in sle_list:
|
||||
if warehouse_account.get(sle.warehouse):
|
||||
# from warehouse account
|
||||
|
||||
sle_rounding_diff += flt(sle.stock_value_difference)
|
||||
|
||||
self.check_expense_account(item_row)
|
||||
|
||||
# expense account/ target_warehouse / source_warehouse
|
||||
@@ -188,6 +190,46 @@ class StockController(AccountsController):
|
||||
elif sle.warehouse not in warehouse_with_no_account:
|
||||
warehouse_with_no_account.append(sle.warehouse)
|
||||
|
||||
if abs(sle_rounding_diff) > (1.0 / (10**precision)) and self.is_internal_transfer():
|
||||
warehouse_asset_account = ""
|
||||
if self.get("is_internal_customer"):
|
||||
warehouse_asset_account = warehouse_account[item_row.get("target_warehouse")]["account"]
|
||||
elif self.get("is_internal_supplier"):
|
||||
warehouse_asset_account = warehouse_account[item_row.get("warehouse")]["account"]
|
||||
|
||||
expense_account = frappe.db.get_value("Company", self.company, "default_expense_account")
|
||||
|
||||
gl_list.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": expense_account,
|
||||
"against": warehouse_asset_account,
|
||||
"cost_center": item_row.cost_center,
|
||||
"project": item_row.project or self.get("project"),
|
||||
"remarks": _("Rounding gain/loss Entry for Stock Transfer"),
|
||||
"debit": sle_rounding_diff,
|
||||
"is_opening": item_row.get("is_opening") or self.get("is_opening") or "No",
|
||||
},
|
||||
warehouse_account[sle.warehouse]["account_currency"],
|
||||
item=item_row,
|
||||
)
|
||||
)
|
||||
|
||||
gl_list.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": warehouse_asset_account,
|
||||
"against": expense_account,
|
||||
"cost_center": item_row.cost_center,
|
||||
"remarks": _("Rounding gain/loss Entry for Stock Transfer"),
|
||||
"credit": sle_rounding_diff,
|
||||
"project": item_row.get("project") or self.get("project"),
|
||||
"is_opening": item_row.get("is_opening") or self.get("is_opening") or "No",
|
||||
},
|
||||
item=item_row,
|
||||
)
|
||||
)
|
||||
|
||||
if warehouse_with_no_account:
|
||||
for wh in warehouse_with_no_account:
|
||||
if frappe.db.get_value("Warehouse", wh, "company"):
|
||||
|
||||
@@ -890,24 +890,33 @@ class calculate_taxes_and_totals(object):
|
||||
self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc)
|
||||
|
||||
def set_total_amount_to_default_mop(self, total_amount_to_pay):
|
||||
default_mode_of_payment = frappe.db.get_value(
|
||||
"POS Payment Method",
|
||||
{"parent": self.doc.pos_profile, "default": 1},
|
||||
["mode_of_payment"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if default_mode_of_payment:
|
||||
self.doc.payments = []
|
||||
self.doc.append(
|
||||
"payments",
|
||||
{
|
||||
"mode_of_payment": default_mode_of_payment.mode_of_payment,
|
||||
"amount": total_amount_to_pay,
|
||||
"default": 1,
|
||||
},
|
||||
total_paid_amount = 0
|
||||
for payment in self.doc.get("payments"):
|
||||
total_paid_amount += (
|
||||
payment.amount if self.doc.party_account_currency == self.doc.currency else payment.base_amount
|
||||
)
|
||||
|
||||
pending_amount = total_amount_to_pay - total_paid_amount
|
||||
|
||||
if pending_amount > 0:
|
||||
default_mode_of_payment = frappe.db.get_value(
|
||||
"POS Payment Method",
|
||||
{"parent": self.doc.pos_profile, "default": 1},
|
||||
["mode_of_payment"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if default_mode_of_payment:
|
||||
self.doc.payments = []
|
||||
self.doc.append(
|
||||
"payments",
|
||||
{
|
||||
"mode_of_payment": default_mode_of_payment.mode_of_payment,
|
||||
"amount": pending_amount,
|
||||
"default": 1,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def get_itemised_tax_breakup_html(doc):
|
||||
if not doc.taxes:
|
||||
|
||||
@@ -11,9 +11,12 @@ frappe.ui.form.on('Course Scheduling Tool', {
|
||||
},
|
||||
refresh(frm) {
|
||||
frm.disable_save();
|
||||
frm.trigger("render_days");
|
||||
frm.page.set_primary_action(__('Schedule Course'), () => {
|
||||
frm.call('schedule_course')
|
||||
frappe.dom.freeze(__("Scheduling..."));
|
||||
frm.call('schedule_course', { days: frm.days.get_checked_options() })
|
||||
.then(r => {
|
||||
frappe.dom.unfreeze();
|
||||
if (!r.message) {
|
||||
frappe.throw(__('There were errors creating Course Schedule'));
|
||||
}
|
||||
@@ -40,5 +43,60 @@ frappe.ui.form.on('Course Scheduling Tool', {
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
render_days: function(frm) {
|
||||
const days_html = $('<div class="days-editor">').appendTo(
|
||||
frm.fields_dict.days_html.wrapper
|
||||
);
|
||||
|
||||
if (!frm.days) {
|
||||
frm.days = frappe.ui.form.make_control({
|
||||
parent: days_html,
|
||||
df: {
|
||||
fieldname: "days",
|
||||
fieldtype: "MultiCheck",
|
||||
select_all: true,
|
||||
columns: 4,
|
||||
options: [
|
||||
{
|
||||
label: __("Monday"),
|
||||
value: "Monday",
|
||||
checked: 0,
|
||||
},
|
||||
{
|
||||
label: __("Tuesday"),
|
||||
value: "Tuesday",
|
||||
checked: 0,
|
||||
},
|
||||
{
|
||||
label: __("Wednesday"),
|
||||
value: "Wednesday",
|
||||
checked: 0,
|
||||
},
|
||||
{
|
||||
label: __("Thursday"),
|
||||
value: "Thursday",
|
||||
checked: 0,
|
||||
},
|
||||
{
|
||||
label: __("Friday"),
|
||||
value: "Friday",
|
||||
checked: 0,
|
||||
},
|
||||
{
|
||||
label: __("Saturday"),
|
||||
value: "Saturday",
|
||||
checked: 0,
|
||||
},
|
||||
{
|
||||
label: __("Sunday"),
|
||||
value: "Sunday",
|
||||
checked: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
render_input: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,661 +1,171 @@
|
||||
{
|
||||
"allow_copy": 1,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2015-09-23 15:37:38.108475",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"editable_grid": 0,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"allow_copy": 1,
|
||||
"creation": "2015-09-23 15:37:38.108475",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"student_group",
|
||||
"course",
|
||||
"program",
|
||||
"column_break_3",
|
||||
"academic_year",
|
||||
"academic_term",
|
||||
"section_break_6",
|
||||
"instructor",
|
||||
"instructor_name",
|
||||
"column_break_9",
|
||||
"room",
|
||||
"section_break_7",
|
||||
"days_html",
|
||||
"section_break_14",
|
||||
"from_time",
|
||||
"course_start_date",
|
||||
"column_break_15",
|
||||
"to_time",
|
||||
"course_end_date",
|
||||
"reschedule"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "student_group",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Student Group",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Student Group",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "student_group",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Student Group",
|
||||
"options": "Student Group",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Course",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Course",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Course",
|
||||
"options": "Course",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "program",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Program",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Program",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "program",
|
||||
"fieldtype": "Link",
|
||||
"label": "Program",
|
||||
"options": "Program",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "academic_year",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Academic Year",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Academic Year",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "academic_year",
|
||||
"fieldtype": "Link",
|
||||
"label": "Academic Year",
|
||||
"options": "Academic Year",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "academic_term",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Academic Term",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Academic Term",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "academic_term",
|
||||
"fieldtype": "Link",
|
||||
"label": "Academic Term",
|
||||
"options": "Academic Term",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "section_break_6",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "section_break_6",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "instructor",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Instructor",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Instructor",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "instructor",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Instructor",
|
||||
"options": "Instructor",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_from": "instructor.instructor_name",
|
||||
"fieldname": "instructor_name",
|
||||
"fieldtype": "Read Only",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Instructor Name",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "instructor_name",
|
||||
"fieldtype": "Read Only",
|
||||
"label": "Instructor Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_9",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "column_break_9",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "room",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Room",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Room",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "room",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Room",
|
||||
"options": "Room",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "",
|
||||
"fieldname": "from_time",
|
||||
"fieldtype": "Time",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "From Time",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "from_time",
|
||||
"fieldtype": "Time",
|
||||
"label": "From Time",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "",
|
||||
"fieldname": "course_start_date",
|
||||
"fieldtype": "Date",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Course Start Date",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "course_start_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Course Start Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "day",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Day",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"default": "0",
|
||||
"fieldname": "reschedule",
|
||||
"fieldtype": "Check",
|
||||
"label": "Reschedule"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "reschedule",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Reschedule",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "column_break_15",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_15",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "to_time",
|
||||
"fieldtype": "Time",
|
||||
"label": "To TIme",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "to_time",
|
||||
"fieldtype": "Time",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "To TIme",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "course_end_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Course End Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "",
|
||||
"fieldname": "course_end_date",
|
||||
"fieldtype": "Date",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Course End Date",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"fieldname": "days_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Days HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_14",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 1,
|
||||
"hide_toolbar": 1,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 1,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": 0,
|
||||
"modified": "2018-05-16 22:43:29.363798",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Education",
|
||||
"name": "Course Scheduling Tool",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-10-01 17:08:07.180557",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Education",
|
||||
"name": "Course Scheduling Tool",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 0,
|
||||
"email": 0,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 0,
|
||||
"read": 1,
|
||||
"report": 0,
|
||||
"role": "Academics User",
|
||||
"set_user_permissions": 0,
|
||||
"share": 0,
|
||||
"submit": 0,
|
||||
"create": 1,
|
||||
"read": 1,
|
||||
"role": "Academics User",
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"restrict_to_domain": "Education",
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
],
|
||||
"restrict_to_domain": "Education",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -14,7 +14,7 @@ from erpnext.education.utils import OverlapError
|
||||
|
||||
class CourseSchedulingTool(Document):
|
||||
@frappe.whitelist()
|
||||
def schedule_course(self):
|
||||
def schedule_course(self, days):
|
||||
"""Creates course schedules as per specified parameters"""
|
||||
|
||||
course_schedules = []
|
||||
@@ -22,7 +22,7 @@ class CourseSchedulingTool(Document):
|
||||
rescheduled = []
|
||||
reschedule_errors = []
|
||||
|
||||
self.validate_mandatory()
|
||||
self.validate_mandatory(days)
|
||||
self.validate_date()
|
||||
self.instructor_name = frappe.db.get_value("Instructor", self.instructor, "instructor_name")
|
||||
|
||||
@@ -34,24 +34,22 @@ class CourseSchedulingTool(Document):
|
||||
self.course = course
|
||||
|
||||
if self.reschedule:
|
||||
rescheduled, reschedule_errors = self.delete_course_schedule(rescheduled, reschedule_errors)
|
||||
rescheduled, reschedule_errors = self.delete_course_schedule(
|
||||
rescheduled, reschedule_errors, days
|
||||
)
|
||||
|
||||
date = self.course_start_date
|
||||
while date < self.course_end_date:
|
||||
if self.day == calendar.day_name[getdate(date).weekday()]:
|
||||
if calendar.day_name[getdate(date).weekday()] in days:
|
||||
course_schedule = self.make_course_schedule(date)
|
||||
try:
|
||||
print("pass")
|
||||
course_schedule.save()
|
||||
except OverlapError:
|
||||
print("fail")
|
||||
course_schedules_errors.append(date)
|
||||
else:
|
||||
course_schedules.append(course_schedule)
|
||||
|
||||
date = add_days(date, 7)
|
||||
else:
|
||||
date = add_days(date, 1)
|
||||
date = add_days(date, 1)
|
||||
|
||||
return dict(
|
||||
course_schedules=course_schedules,
|
||||
@@ -60,8 +58,10 @@ class CourseSchedulingTool(Document):
|
||||
reschedule_errors=reschedule_errors,
|
||||
)
|
||||
|
||||
def validate_mandatory(self):
|
||||
def validate_mandatory(self, days):
|
||||
"""Validates all mandatory fields"""
|
||||
if not days:
|
||||
frappe.throw(_("Please select at least one day to schedule the course."))
|
||||
|
||||
fields = [
|
||||
"course",
|
||||
@@ -71,7 +71,6 @@ class CourseSchedulingTool(Document):
|
||||
"to_time",
|
||||
"course_start_date",
|
||||
"course_end_date",
|
||||
"day",
|
||||
]
|
||||
for d in fields:
|
||||
if not self.get(d):
|
||||
@@ -82,9 +81,8 @@ class CourseSchedulingTool(Document):
|
||||
if self.course_start_date > self.course_end_date:
|
||||
frappe.throw(_("Course Start Date cannot be greater than Course End Date."))
|
||||
|
||||
def delete_course_schedule(self, rescheduled, reschedule_errors):
|
||||
def delete_course_schedule(self, rescheduled, reschedule_errors, days):
|
||||
"""Delete all course schedule within the Date range and specified filters"""
|
||||
|
||||
schedules = frappe.get_list(
|
||||
"Course Schedule",
|
||||
fields=["name", "schedule_date"],
|
||||
@@ -98,7 +96,7 @@ class CourseSchedulingTool(Document):
|
||||
|
||||
for d in schedules:
|
||||
try:
|
||||
if self.day == calendar.day_name[getdate(d.schedule_date).weekday()]:
|
||||
if calendar.day_name[getdate(d.schedule_date).weekday()] in days:
|
||||
frappe.delete_doc("Course Schedule", d.name)
|
||||
rescheduled.append(d.name)
|
||||
except Exception:
|
||||
@@ -108,7 +106,6 @@ class CourseSchedulingTool(Document):
|
||||
def make_course_schedule(self, date):
|
||||
"""Makes a new Course Schedule.
|
||||
:param date: Date on which Course Schedule will be created."""
|
||||
|
||||
course_schedule = frappe.new_doc("Course Schedule")
|
||||
course_schedule.student_group = self.student_group
|
||||
course_schedule.course = self.course
|
||||
|
||||
@@ -334,8 +334,6 @@ has_website_permission = {
|
||||
"Patient": "erpnext.healthcare.web_form.personal_details.personal_details.has_website_permission",
|
||||
}
|
||||
|
||||
dump_report_map = "erpnext.startup.report_data_map.data_map"
|
||||
|
||||
before_tests = "erpnext.setup.utils.before_tests"
|
||||
|
||||
standard_queries = {
|
||||
@@ -478,7 +476,6 @@ scheduler_events = {
|
||||
],
|
||||
"hourly": [
|
||||
"erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails",
|
||||
"erpnext.accounts.doctype.subscription.subscription.process_all",
|
||||
"erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_settings.schedule_get_order_details",
|
||||
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
|
||||
"erpnext.projects.doctype.project.project.hourly_reminder",
|
||||
@@ -487,6 +484,7 @@ scheduler_events = {
|
||||
"erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders",
|
||||
],
|
||||
"hourly_long": [
|
||||
"erpnext.accounts.doctype.subscription.subscription.process_all",
|
||||
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
|
||||
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
|
||||
],
|
||||
|
||||
@@ -253,10 +253,12 @@ def get_unmarked_days(employee, month, exclude_holidays=0):
|
||||
start_day = 1
|
||||
end_day = calendar.monthrange(today.year, month_map[month])[1] + 1
|
||||
|
||||
if joining_date and joining_date.month == month_map[month]:
|
||||
if joining_date and joining_date.year == today.year and joining_date.month == month_map[month]:
|
||||
start_day = joining_date.day
|
||||
|
||||
if relieving_date and relieving_date.month == month_map[month]:
|
||||
if (
|
||||
relieving_date and relieving_date.year == today.year and relieving_date.month == month_map[month]
|
||||
):
|
||||
end_day = relieving_date.day + 1
|
||||
|
||||
dates_of_month = [
|
||||
|
||||
@@ -13,6 +13,8 @@ frappe.listview_settings['Attendance'] = {
|
||||
onload: function(list_view) {
|
||||
let me = this;
|
||||
const months = moment.months();
|
||||
const curMonth = moment().format("MMMM");
|
||||
months.splice(months.indexOf(curMonth) + 1);
|
||||
list_view.page.add_inner_button(__("Mark Attendance"), function() {
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __("Mark Attendance"),
|
||||
|
||||
@@ -181,7 +181,6 @@ def add_data(
|
||||
total_l += 1
|
||||
elif status == "Half Day":
|
||||
total_p += 0.5
|
||||
total_a += 0.5
|
||||
total_l += 0.5
|
||||
elif not status:
|
||||
total_um += 1
|
||||
|
||||
@@ -61,6 +61,10 @@ frappe.ui.form.on('Loan', {
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
if (frm.doc.repayment_schedule_type == "Pro-rated calendar months") {
|
||||
frm.set_df_property("repayment_start_date", "label", "Interest Calculation Start Date");
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus == 1) {
|
||||
if (["Disbursed", "Partially Disbursed"].includes(frm.doc.status) && (!frm.doc.repay_from_salary)) {
|
||||
frm.add_custom_button(__('Request Loan Closure'), function() {
|
||||
@@ -103,6 +107,14 @@ frappe.ui.form.on('Loan', {
|
||||
frm.trigger("toggle_fields");
|
||||
},
|
||||
|
||||
repayment_schedule_type: function(frm) {
|
||||
if (frm.doc.repayment_schedule_type == "Pro-rated calendar months") {
|
||||
frm.set_df_property("repayment_start_date", "label", "Interest Calculation Start Date");
|
||||
} else {
|
||||
frm.set_df_property("repayment_start_date", "label", "Repayment Start Date");
|
||||
}
|
||||
},
|
||||
|
||||
loan_type: function(frm) {
|
||||
frm.toggle_reqd("repayment_method", frm.doc.is_term_loan);
|
||||
frm.toggle_display("repayment_method", frm.doc.is_term_loan);
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"manually_update_paid_amount_in_salary_slip",
|
||||
"section_break_8",
|
||||
"loan_type",
|
||||
"repayment_schedule_type",
|
||||
"loan_amount",
|
||||
"rate_of_interest",
|
||||
"is_secured_loan",
|
||||
@@ -167,7 +168,8 @@
|
||||
"depends_on": "is_term_loan",
|
||||
"fieldname": "repayment_start_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Repayment Start Date"
|
||||
"label": "Repayment Start Date",
|
||||
"mandatory_depends_on": "is_term_loan"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_11",
|
||||
@@ -419,12 +421,20 @@
|
||||
"fieldname": "manually_update_paid_amount_in_salary_slip",
|
||||
"fieldtype": "Check",
|
||||
"label": "Manually Update Paid Amount in Salary Slip"
|
||||
},
|
||||
{
|
||||
"depends_on": "is_term_loan",
|
||||
"fetch_from": "loan_type.repayment_schedule_type",
|
||||
"fieldname": "repayment_schedule_type",
|
||||
"fieldtype": "Data",
|
||||
"label": "Repayment Schedule Type",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-09-13 02:05:25.017190",
|
||||
"modified": "2022-11-01 10:36:47.902903",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan",
|
||||
|
||||
@@ -7,7 +7,16 @@ import math
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import add_months, flt, get_last_day, getdate, now_datetime, nowdate
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
date_diff,
|
||||
flt,
|
||||
get_last_day,
|
||||
getdate,
|
||||
now_datetime,
|
||||
nowdate,
|
||||
)
|
||||
from six import string_types
|
||||
|
||||
import erpnext
|
||||
@@ -115,30 +124,81 @@ class Loan(AccountsController):
|
||||
if not self.repayment_start_date:
|
||||
frappe.throw(_("Repayment Start Date is mandatory for term loans"))
|
||||
|
||||
schedule_type_details = frappe.db.get_value(
|
||||
"Loan Type", self.loan_type, ["repayment_schedule_type", "repayment_date_on"], as_dict=1
|
||||
)
|
||||
|
||||
self.repayment_schedule = []
|
||||
payment_date = self.repayment_start_date
|
||||
balance_amount = self.loan_amount
|
||||
while balance_amount > 0:
|
||||
interest_amount = flt(balance_amount * flt(self.rate_of_interest) / (12 * 100))
|
||||
principal_amount = self.monthly_repayment_amount - interest_amount
|
||||
balance_amount = flt(balance_amount + interest_amount - self.monthly_repayment_amount)
|
||||
if balance_amount < 0:
|
||||
principal_amount += balance_amount
|
||||
balance_amount = 0.0
|
||||
|
||||
total_payment = principal_amount + interest_amount
|
||||
self.append(
|
||||
"repayment_schedule",
|
||||
{
|
||||
"payment_date": payment_date,
|
||||
"principal_amount": principal_amount,
|
||||
"interest_amount": interest_amount,
|
||||
"total_payment": total_payment,
|
||||
"balance_loan_amount": balance_amount,
|
||||
},
|
||||
while balance_amount > 0:
|
||||
interest_amount, principal_amount, balance_amount, total_payment = self.get_amounts(
|
||||
payment_date,
|
||||
balance_amount,
|
||||
schedule_type_details.repayment_schedule_type,
|
||||
schedule_type_details.repayment_date_on,
|
||||
)
|
||||
next_payment_date = add_single_month(payment_date)
|
||||
payment_date = next_payment_date
|
||||
|
||||
if schedule_type_details.repayment_schedule_type == "Pro-rated calendar months":
|
||||
next_payment_date = get_last_day(payment_date)
|
||||
if schedule_type_details.repayment_date_on == "Start of the next month":
|
||||
next_payment_date = add_days(next_payment_date, 1)
|
||||
|
||||
payment_date = next_payment_date
|
||||
|
||||
self.add_repayment_schedule_row(
|
||||
payment_date, principal_amount, interest_amount, total_payment, balance_amount
|
||||
)
|
||||
|
||||
if (
|
||||
schedule_type_details.repayment_schedule_type == "Monthly as per repayment start date"
|
||||
or schedule_type_details.repayment_date_on == "End of the current month"
|
||||
):
|
||||
next_payment_date = add_single_month(payment_date)
|
||||
payment_date = next_payment_date
|
||||
|
||||
def get_amounts(self, payment_date, balance_amount, schedule_type, repayment_date_on):
|
||||
if schedule_type == "Monthly as per repayment start date":
|
||||
days = 1
|
||||
months = 12
|
||||
else:
|
||||
expected_payment_date = get_last_day(payment_date)
|
||||
if repayment_date_on == "Start of the next month":
|
||||
expected_payment_date = add_days(expected_payment_date, 1)
|
||||
|
||||
if expected_payment_date == payment_date:
|
||||
# using 30 days for calculating interest for all full months
|
||||
days = 30
|
||||
months = 365
|
||||
else:
|
||||
days = date_diff(get_last_day(payment_date), payment_date)
|
||||
months = 365
|
||||
|
||||
interest_amount = flt(balance_amount * flt(self.rate_of_interest) * days / (months * 100))
|
||||
principal_amount = self.monthly_repayment_amount - interest_amount
|
||||
balance_amount = flt(balance_amount + interest_amount - self.monthly_repayment_amount)
|
||||
if balance_amount < 0:
|
||||
principal_amount += balance_amount
|
||||
balance_amount = 0.0
|
||||
|
||||
total_payment = principal_amount + interest_amount
|
||||
|
||||
return interest_amount, principal_amount, balance_amount, total_payment
|
||||
|
||||
def add_repayment_schedule_row(
|
||||
self, payment_date, principal_amount, interest_amount, total_payment, balance_loan_amount
|
||||
):
|
||||
self.append(
|
||||
"repayment_schedule",
|
||||
{
|
||||
"payment_date": payment_date,
|
||||
"principal_amount": principal_amount,
|
||||
"interest_amount": interest_amount,
|
||||
"total_payment": total_payment,
|
||||
"balance_loan_amount": balance_loan_amount,
|
||||
},
|
||||
)
|
||||
|
||||
def set_repayment_period(self):
|
||||
if self.repayment_method == "Repay Fixed Amount per Period":
|
||||
|
||||
@@ -4,7 +4,16 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, add_months, add_to_date, date_diff, flt, get_datetime, nowdate
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
add_to_date,
|
||||
date_diff,
|
||||
flt,
|
||||
format_date,
|
||||
get_datetime,
|
||||
nowdate,
|
||||
)
|
||||
|
||||
from erpnext.loan_management.doctype.loan.loan import (
|
||||
make_loan_write_off,
|
||||
@@ -50,6 +59,51 @@ class TestLoan(unittest.TestCase):
|
||||
loan_account="Loan Account - _TC",
|
||||
interest_income_account="Interest Income Account - _TC",
|
||||
penalty_income_account="Penalty Income Account - _TC",
|
||||
repayment_schedule_type="Monthly as per repayment start date",
|
||||
)
|
||||
|
||||
create_loan_type(
|
||||
"Term Loan Type 1",
|
||||
12000,
|
||||
7.5,
|
||||
is_term_loan=1,
|
||||
mode_of_payment="Cash",
|
||||
disbursement_account="Disbursement Account - _TC",
|
||||
payment_account="Payment Account - _TC",
|
||||
loan_account="Loan Account - _TC",
|
||||
interest_income_account="Interest Income Account - _TC",
|
||||
penalty_income_account="Penalty Income Account - _TC",
|
||||
repayment_schedule_type="Monthly as per repayment start date",
|
||||
)
|
||||
|
||||
create_loan_type(
|
||||
"Term Loan Type 2",
|
||||
12000,
|
||||
7.5,
|
||||
is_term_loan=1,
|
||||
mode_of_payment="Cash",
|
||||
disbursement_account="Disbursement Account - _TC",
|
||||
payment_account="Payment Account - _TC",
|
||||
loan_account="Loan Account - _TC",
|
||||
interest_income_account="Interest Income Account - _TC",
|
||||
penalty_income_account="Penalty Income Account - _TC",
|
||||
repayment_schedule_type="Pro-rated calendar months",
|
||||
repayment_date_on="Start of the next month",
|
||||
)
|
||||
|
||||
create_loan_type(
|
||||
"Term Loan Type 3",
|
||||
12000,
|
||||
7.5,
|
||||
is_term_loan=1,
|
||||
mode_of_payment="Cash",
|
||||
disbursement_account="Disbursement Account - _TC",
|
||||
payment_account="Payment Account - _TC",
|
||||
loan_account="Loan Account - _TC",
|
||||
interest_income_account="Interest Income Account - _TC",
|
||||
penalty_income_account="Penalty Income Account - _TC",
|
||||
repayment_schedule_type="Pro-rated calendar months",
|
||||
repayment_date_on="End of the current month",
|
||||
)
|
||||
|
||||
create_loan_type(
|
||||
@@ -65,6 +119,7 @@ class TestLoan(unittest.TestCase):
|
||||
"Loan Account - _TC",
|
||||
"Interest Income Account - _TC",
|
||||
"Penalty Income Account - _TC",
|
||||
repayment_schedule_type="Monthly as per repayment start date",
|
||||
)
|
||||
|
||||
create_loan_type(
|
||||
@@ -912,6 +967,69 @@ class TestLoan(unittest.TestCase):
|
||||
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
|
||||
self.assertEqual(flt(amounts["pending_principal_amount"], 0), 0)
|
||||
|
||||
def test_term_loan_schedule_types(self):
|
||||
loan = create_loan(
|
||||
self.applicant1,
|
||||
"Term Loan Type 1",
|
||||
12000,
|
||||
"Repay Over Number of Periods",
|
||||
12,
|
||||
repayment_start_date="2022-10-17",
|
||||
)
|
||||
|
||||
# Check for first, second and last installment date
|
||||
self.assertEqual(
|
||||
format_date(loan.get("repayment_schedule")[0].payment_date, "dd-MM-yyyy"), "17-10-2022"
|
||||
)
|
||||
self.assertEqual(
|
||||
format_date(loan.get("repayment_schedule")[1].payment_date, "dd-MM-yyyy"), "17-11-2022"
|
||||
)
|
||||
self.assertEqual(
|
||||
format_date(loan.get("repayment_schedule")[-1].payment_date, "dd-MM-yyyy"), "17-09-2023"
|
||||
)
|
||||
|
||||
loan.loan_type = "Term Loan Type 2"
|
||||
loan.save()
|
||||
|
||||
# Check for first, second and last installment date
|
||||
self.assertEqual(
|
||||
format_date(loan.get("repayment_schedule")[0].payment_date, "dd-MM-yyyy"), "01-11-2022"
|
||||
)
|
||||
self.assertEqual(
|
||||
format_date(loan.get("repayment_schedule")[1].payment_date, "dd-MM-yyyy"), "01-12-2022"
|
||||
)
|
||||
self.assertEqual(
|
||||
format_date(loan.get("repayment_schedule")[-1].payment_date, "dd-MM-yyyy"), "01-10-2023"
|
||||
)
|
||||
|
||||
loan.loan_type = "Term Loan Type 3"
|
||||
loan.save()
|
||||
|
||||
# Check for first, second and last installment date
|
||||
self.assertEqual(
|
||||
format_date(loan.get("repayment_schedule")[0].payment_date, "dd-MM-yyyy"), "31-10-2022"
|
||||
)
|
||||
self.assertEqual(
|
||||
format_date(loan.get("repayment_schedule")[1].payment_date, "dd-MM-yyyy"), "30-11-2022"
|
||||
)
|
||||
self.assertEqual(
|
||||
format_date(loan.get("repayment_schedule")[-1].payment_date, "dd-MM-yyyy"), "30-09-2023"
|
||||
)
|
||||
|
||||
loan.repayment_method = "Repay Fixed Amount per Period"
|
||||
loan.monthly_repayment_amount = 1042
|
||||
loan.save()
|
||||
|
||||
self.assertEqual(
|
||||
format_date(loan.get("repayment_schedule")[0].payment_date, "dd-MM-yyyy"), "31-10-2022"
|
||||
)
|
||||
self.assertEqual(
|
||||
format_date(loan.get("repayment_schedule")[1].payment_date, "dd-MM-yyyy"), "30-11-2022"
|
||||
)
|
||||
self.assertEqual(
|
||||
format_date(loan.get("repayment_schedule")[-1].payment_date, "dd-MM-yyyy"), "30-09-2023"
|
||||
)
|
||||
|
||||
|
||||
def create_loan_scenario_for_penalty(doc):
|
||||
pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}]
|
||||
@@ -1043,6 +1161,8 @@ def create_loan_type(
|
||||
penalty_income_account=None,
|
||||
repayment_method=None,
|
||||
repayment_periods=None,
|
||||
repayment_schedule_type=None,
|
||||
repayment_date_on=None,
|
||||
):
|
||||
|
||||
if not frappe.db.exists("Loan Type", loan_name):
|
||||
@@ -1052,6 +1172,7 @@ def create_loan_type(
|
||||
"company": "_Test Company",
|
||||
"loan_name": loan_name,
|
||||
"is_term_loan": is_term_loan,
|
||||
"repayment_schedule_type": "Monthly as per repayment start date",
|
||||
"maximum_loan_amount": maximum_loan_amount,
|
||||
"rate_of_interest": rate_of_interest,
|
||||
"penalty_interest_rate": penalty_interest_rate,
|
||||
@@ -1066,8 +1187,14 @@ def create_loan_type(
|
||||
"repayment_periods": repayment_periods,
|
||||
"write_off_amount": 100,
|
||||
}
|
||||
).insert()
|
||||
)
|
||||
|
||||
if loan_type.is_term_loan:
|
||||
loan_type.repayment_schedule_type = repayment_schedule_type
|
||||
if loan_type.repayment_schedule_type != "Monthly as per repayment start date":
|
||||
loan_type.repayment_date_on = repayment_date_on
|
||||
|
||||
loan_type.insert()
|
||||
loan_type.submit()
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
"company",
|
||||
"is_term_loan",
|
||||
"disabled",
|
||||
"repayment_schedule_type",
|
||||
"repayment_date_on",
|
||||
"description",
|
||||
"account_details_section",
|
||||
"mode_of_payment",
|
||||
@@ -157,12 +159,30 @@
|
||||
"label": "Disbursement Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "is_term_loan",
|
||||
"description": "The schedule type that will be used for generating the term loan schedules (will affect the payment date and monthly repayment amount)",
|
||||
"fieldname": "repayment_schedule_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Repayment Schedule Type",
|
||||
"mandatory_depends_on": "is_term_loan",
|
||||
"options": "\nMonthly as per repayment start date\nPro-rated calendar months"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.repayment_schedule_type == \"Pro-rated calendar months\"",
|
||||
"description": "Select whether the repayment date should be the end of the current month or start of the upcoming month",
|
||||
"fieldname": "repayment_date_on",
|
||||
"fieldtype": "Select",
|
||||
"label": "Repayment Date On",
|
||||
"mandatory_depends_on": "eval:doc.repayment_schedule_type == \"Pro-rated calendar months\"",
|
||||
"options": "\nStart of the next month\nEnd of the current month"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-01-25 16:23:57.009349",
|
||||
"modified": "2022-11-01 17:43:03.954201",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Type",
|
||||
|
||||
@@ -384,6 +384,7 @@ class BOM(WebsiteGenerator):
|
||||
if self.docstatus == 2:
|
||||
return
|
||||
|
||||
self.flags.cost_updated = False
|
||||
existing_bom_cost = self.total_cost
|
||||
|
||||
if self.docstatus == 1:
|
||||
@@ -406,7 +407,11 @@ class BOM(WebsiteGenerator):
|
||||
frappe.get_doc("BOM", bom).update_cost(from_child_bom=True)
|
||||
|
||||
if not from_child_bom:
|
||||
frappe.msgprint(_("Cost Updated"), alert=True)
|
||||
msg = "Cost Updated"
|
||||
if not self.flags.cost_updated:
|
||||
msg = "No changes in cost found"
|
||||
|
||||
frappe.msgprint(_(msg), alert=True)
|
||||
|
||||
def update_parent_cost(self):
|
||||
if self.total_cost:
|
||||
@@ -592,11 +597,16 @@ class BOM(WebsiteGenerator):
|
||||
# not via doc event, table is not regenerated and needs updation
|
||||
self.calculate_exploded_cost()
|
||||
|
||||
old_cost = self.total_cost
|
||||
|
||||
self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost
|
||||
self.base_total_cost = (
|
||||
self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost
|
||||
)
|
||||
|
||||
if self.total_cost != old_cost:
|
||||
self.flags.cost_updated = True
|
||||
|
||||
def calculate_op_cost(self, update_hour_rate=False):
|
||||
"""Update workstation rate and calculates totals"""
|
||||
self.operating_cost = 0
|
||||
|
||||
@@ -583,6 +583,28 @@ class TestBOM(FrappeTestCase):
|
||||
bom.submit()
|
||||
self.assertEqual(bom.exploded_items[0].rate, bom.items[0].base_rate)
|
||||
|
||||
def test_bom_cost_update_flag(self):
|
||||
rm_item = make_item(
|
||||
properties={"is_stock_item": 1, "valuation_rate": 99, "last_purchase_rate": 89}
|
||||
).name
|
||||
fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
|
||||
bom = make_bom(item=fg_item, raw_materials=[rm_item])
|
||||
|
||||
create_stock_reconciliation(
|
||||
item_code=rm_item, warehouse="_Test Warehouse - _TC", qty=100, rate=600
|
||||
)
|
||||
|
||||
bom.load_from_db()
|
||||
bom.update_cost()
|
||||
self.assertTrue(bom.flags.cost_updated)
|
||||
|
||||
bom.load_from_db()
|
||||
bom.update_cost()
|
||||
self.assertFalse(bom.flags.cost_updated)
|
||||
|
||||
|
||||
def get_default_bom(item_code="_Test FG Item 2"):
|
||||
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
|
||||
|
||||
@@ -133,7 +133,7 @@ class JobCard(Document):
|
||||
(%(from_time)s <= jctl.from_time and %(to_time)s >= jctl.to_time) {0}
|
||||
)
|
||||
and jctl.name != %(name)s and jc.name != %(parent)s and jc.docstatus < 2 {1}
|
||||
order by jctl.to_time desc limit 1""".format(
|
||||
order by jctl.to_time desc""".format(
|
||||
extra_cond, validate_overlap_for
|
||||
),
|
||||
{
|
||||
|
||||
@@ -133,6 +133,45 @@ class TestJobCard(FrappeTestCase):
|
||||
)
|
||||
self.assertRaises(OverlapError, jc2.save)
|
||||
|
||||
def test_job_card_overlap_with_capacity(self):
|
||||
wo2 = make_wo_order_test_record(item="_Test FG Item 2", qty=2)
|
||||
|
||||
workstation = make_workstation(workstation_name=random_string(5)).name
|
||||
frappe.db.set_value("Workstation", workstation, "production_capacity", 1)
|
||||
|
||||
jc1 = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
|
||||
jc2 = frappe.get_last_doc("Job Card", {"work_order": wo2.name})
|
||||
|
||||
jc1.workstation = workstation
|
||||
jc1.append(
|
||||
"time_logs",
|
||||
{"from_time": "2021-01-01 00:00:00", "to_time": "2021-01-01 08:00:00", "completed_qty": 1},
|
||||
)
|
||||
jc1.save()
|
||||
|
||||
jc2.workstation = workstation
|
||||
|
||||
# add a new entry in same time slice
|
||||
jc2.append(
|
||||
"time_logs",
|
||||
{"from_time": "2021-01-01 00:01:00", "to_time": "2021-01-01 06:00:00", "completed_qty": 1},
|
||||
)
|
||||
self.assertRaises(OverlapError, jc2.save)
|
||||
|
||||
frappe.db.set_value("Workstation", workstation, "production_capacity", 2)
|
||||
jc2.load_from_db()
|
||||
|
||||
jc2.workstation = workstation
|
||||
|
||||
# add a new entry in same time slice
|
||||
jc2.append(
|
||||
"time_logs",
|
||||
{"from_time": "2021-01-01 00:01:00", "to_time": "2021-01-01 06:00:00", "completed_qty": 1},
|
||||
)
|
||||
|
||||
jc2.save()
|
||||
self.assertTrue(jc2.name)
|
||||
|
||||
def test_job_card_multiple_materials_transfer(self):
|
||||
"Test transferring RMs separately against Job Card with multiple RMs."
|
||||
self.transfer_material_against = "Job Card"
|
||||
|
||||
@@ -27,6 +27,7 @@ from six import iteritems
|
||||
from erpnext.manufacturing.doctype.bom.bom import get_children, validate_bom_no
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
|
||||
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
||||
from erpnext.stock.get_item_details import get_conversion_factor
|
||||
from erpnext.utilities.transaction_base import validate_uom_is_integer
|
||||
|
||||
|
||||
@@ -651,13 +652,23 @@ class ProductionPlan(Document):
|
||||
else:
|
||||
material_request = material_request_map[key]
|
||||
|
||||
conversion_factor = 1.0
|
||||
if (
|
||||
material_request_type == "Purchase"
|
||||
and item_doc.purchase_uom
|
||||
and item_doc.purchase_uom != item_doc.stock_uom
|
||||
):
|
||||
conversion_factor = (
|
||||
get_conversion_factor(item_doc.name, item_doc.purchase_uom).get("conversion_factor") or 1.0
|
||||
)
|
||||
|
||||
# add item
|
||||
material_request.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": item.item_code,
|
||||
"from_warehouse": item.from_warehouse,
|
||||
"qty": item.quantity,
|
||||
"qty": item.quantity / conversion_factor,
|
||||
"schedule_date": schedule_date,
|
||||
"warehouse": item.warehouse,
|
||||
"sales_order": item.sales_order,
|
||||
@@ -829,6 +840,7 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p
|
||||
.select(
|
||||
(IfNull(Sum(bei.stock_qty / IfNull(bom.quantity, 1)), 0) * planned_qty).as_("qty"),
|
||||
item.item_name,
|
||||
item.name.as_("item_code"),
|
||||
bei.description,
|
||||
bei.stock_uom,
|
||||
item.min_order_qty,
|
||||
|
||||
@@ -732,6 +732,35 @@ class TestProductionPlan(FrappeTestCase):
|
||||
self.assertEqual(pln.status, "Completed")
|
||||
self.assertEqual(pln.po_items[0].produced_qty, 5)
|
||||
|
||||
def test_material_request_item_for_purchase_uom(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
fg_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name
|
||||
bom_item = make_item(
|
||||
properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1", "purchase_uom": "Nos"}
|
||||
).name
|
||||
|
||||
if not frappe.db.exists("UOM Conversion Detail", {"parent": bom_item, "uom": "Nos"}):
|
||||
doc = frappe.get_doc("Item", bom_item)
|
||||
doc.append("uoms", {"uom": "Nos", "conversion_factor": 10})
|
||||
doc.save()
|
||||
|
||||
make_bom(item=fg_item, raw_materials=[bom_item], source_warehouse="_Test Warehouse - _TC")
|
||||
|
||||
pln = create_production_plan(
|
||||
item_code=fg_item, planned_qty=10, ignore_existing_ordered_qty=1, stock_uom="_Test UOM 1"
|
||||
)
|
||||
|
||||
pln.make_material_request()
|
||||
for row in frappe.get_all(
|
||||
"Material Request Item",
|
||||
filters={"production_plan": pln.name},
|
||||
fields=["item_code", "uom", "qty"],
|
||||
):
|
||||
self.assertEqual(row.item_code, bom_item)
|
||||
self.assertEqual(row.uom, "Nos")
|
||||
self.assertEqual(row.qty, 1)
|
||||
|
||||
|
||||
def create_production_plan(**args):
|
||||
"""
|
||||
|
||||
@@ -555,13 +555,10 @@ erpnext.work_order = {
|
||||
}
|
||||
}
|
||||
|
||||
if(!frm.doc.skip_transfer){
|
||||
if (frm.doc.status != 'Stopped') {
|
||||
// If "Material Consumption is check in Manufacturing Settings, allow Material Consumption
|
||||
if ((flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing))
|
||||
&& frm.doc.status != 'Stopped') {
|
||||
frm.has_finish_btn = true;
|
||||
|
||||
if (frm.doc.__onload && frm.doc.__onload.material_consumption == 1) {
|
||||
if (frm.doc.__onload && frm.doc.__onload.material_consumption == 1) {
|
||||
if (flt(doc.material_transferred_for_manufacturing) > 0 || frm.doc.skip_transfer) {
|
||||
// Only show "Material Consumption" when required_qty > consumed_qty
|
||||
var counter = 0;
|
||||
var tbl = frm.doc.required_items || [];
|
||||
@@ -580,26 +577,47 @@ erpnext.work_order = {
|
||||
consumption_btn.addClass('btn-primary');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var finish_btn = frm.add_custom_button(__('Finish'), function() {
|
||||
erpnext.work_order.make_se(frm, 'Manufacture');
|
||||
});
|
||||
if(!frm.doc.skip_transfer){
|
||||
if (flt(doc.material_transferred_for_manufacturing) > 0) {
|
||||
if ((flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing))) {
|
||||
frm.has_finish_btn = true;
|
||||
|
||||
if(doc.material_transferred_for_manufacturing>=doc.qty) {
|
||||
// all materials transferred for manufacturing, make this primary
|
||||
var finish_btn = frm.add_custom_button(__('Finish'), function() {
|
||||
erpnext.work_order.make_se(frm, 'Manufacture');
|
||||
});
|
||||
|
||||
if(doc.material_transferred_for_manufacturing>=doc.qty) {
|
||||
// all materials transferred for manufacturing, make this primary
|
||||
finish_btn.addClass('btn-primary');
|
||||
}
|
||||
} else {
|
||||
frappe.db.get_doc("Manufacturing Settings").then((doc) => {
|
||||
let allowance_percentage = doc.overproduction_percentage_for_work_order;
|
||||
|
||||
if (allowance_percentage > 0) {
|
||||
let allowed_qty = frm.doc.qty + ((allowance_percentage / 100) * frm.doc.qty);
|
||||
|
||||
if ((flt(doc.produced_qty) < allowed_qty)) {
|
||||
frm.add_custom_button(__('Finish'), function() {
|
||||
erpnext.work_order.make_se(frm, 'Manufacture');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ((flt(doc.produced_qty) < flt(doc.qty))) {
|
||||
var finish_btn = frm.add_custom_button(__('Finish'), function() {
|
||||
erpnext.work_order.make_se(frm, 'Manufacture');
|
||||
});
|
||||
finish_btn.addClass('btn-primary');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ((flt(doc.produced_qty) < flt(doc.qty)) && frm.doc.status != 'Stopped') {
|
||||
var finish_btn = frm.add_custom_button(__('Finish'), function() {
|
||||
erpnext.work_order.make_se(frm, 'Manufacture');
|
||||
});
|
||||
finish_btn.addClass('btn-primary');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
calculate_cost: function(doc) {
|
||||
if (doc.operations){
|
||||
|
||||
@@ -146,7 +146,7 @@ def get_bom_data(filters):
|
||||
)
|
||||
)
|
||||
else:
|
||||
query = query.where(bin.warehouse == frappe.db.escape(filters.get("warehouse")))
|
||||
query = query.where(bin.warehouse == filters.get("warehouse"))
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Floor, Sum
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -11,7 +13,6 @@ def execute(filters=None):
|
||||
filters = {}
|
||||
|
||||
columns = get_columns()
|
||||
|
||||
data = get_bom_stock(filters)
|
||||
|
||||
return columns, data
|
||||
@@ -33,59 +34,57 @@ def get_columns():
|
||||
|
||||
|
||||
def get_bom_stock(filters):
|
||||
conditions = ""
|
||||
bom = filters.get("bom")
|
||||
|
||||
table = "`tabBOM Item`"
|
||||
qty_field = "stock_qty"
|
||||
|
||||
qty_to_produce = filters.get("qty_to_produce", 1)
|
||||
if int(qty_to_produce) <= 0:
|
||||
qty_to_produce = filters.get("qty_to_produce") or 1
|
||||
if int(qty_to_produce) < 0:
|
||||
frappe.throw(_("Quantity to Produce can not be less than Zero"))
|
||||
|
||||
if filters.get("show_exploded_view"):
|
||||
table = "`tabBOM Explosion Item`"
|
||||
bom_item_table = "BOM Explosion Item"
|
||||
else:
|
||||
bom_item_table = "BOM Item"
|
||||
|
||||
bin = frappe.qb.DocType("Bin")
|
||||
bom = frappe.qb.DocType("BOM")
|
||||
bom_item = frappe.qb.DocType(bom_item_table)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(bom)
|
||||
.inner_join(bom_item)
|
||||
.on(bom.name == bom_item.parent)
|
||||
.left_join(bin)
|
||||
.on(bom_item.item_code == bin.item_code)
|
||||
.select(
|
||||
bom_item.item_code,
|
||||
bom_item.description,
|
||||
bom_item.stock_qty,
|
||||
bom_item.stock_uom,
|
||||
bom_item.stock_qty * qty_to_produce / bom.quantity,
|
||||
Sum(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"))
|
||||
.groupby(bom_item.item_code)
|
||||
)
|
||||
|
||||
if filters.get("warehouse"):
|
||||
warehouse_details = frappe.db.get_value(
|
||||
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
|
||||
)
|
||||
|
||||
if warehouse_details:
|
||||
conditions += (
|
||||
" and exists (select name from `tabWarehouse` wh \
|
||||
where wh.lft >= %s and wh.rgt <= %s and ledger.warehouse = wh.name)"
|
||||
% (warehouse_details.lft, warehouse_details.rgt)
|
||||
wh = frappe.qb.DocType("Warehouse")
|
||||
query = query.where(
|
||||
ExistsCriterion(
|
||||
frappe.qb.from_(wh)
|
||||
.select(wh.name)
|
||||
.where(
|
||||
(wh.lft >= warehouse_details.lft)
|
||||
& (wh.rgt <= warehouse_details.rgt)
|
||||
& (bin.warehouse == wh.name)
|
||||
)
|
||||
)
|
||||
)
|
||||
else:
|
||||
conditions += " and ledger.warehouse = %s" % frappe.db.escape(filters.get("warehouse"))
|
||||
query = query.where(bin.warehouse == filters.get("warehouse"))
|
||||
|
||||
else:
|
||||
conditions += ""
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
bom_item.item_code,
|
||||
bom_item.description ,
|
||||
bom_item.{qty_field},
|
||||
bom_item.stock_uom,
|
||||
bom_item.{qty_field} * {qty_to_produce} / bom.quantity,
|
||||
sum(ledger.actual_qty) as actual_qty,
|
||||
sum(FLOOR(ledger.actual_qty / (bom_item.{qty_field} * {qty_to_produce} / bom.quantity)))
|
||||
FROM
|
||||
`tabBOM` AS bom INNER JOIN {table} AS bom_item
|
||||
ON bom.name = bom_item.parent
|
||||
LEFT JOIN `tabBin` AS ledger
|
||||
ON bom_item.item_code = ledger.item_code
|
||||
{conditions}
|
||||
WHERE
|
||||
bom_item.parent = {bom} and bom_item.parenttype='BOM'
|
||||
|
||||
GROUP BY bom_item.item_code""".format(
|
||||
qty_field=qty_field,
|
||||
table=table,
|
||||
conditions=conditions,
|
||||
bom=frappe.db.escape(bom),
|
||||
qty_to_produce=qty_to_produce or 1,
|
||||
)
|
||||
)
|
||||
return query.run()
|
||||
|
||||
@@ -64,22 +64,21 @@ def get_columns(filters):
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
cond = "1=1"
|
||||
wo = frappe.qb.DocType("Work Order")
|
||||
query = (
|
||||
frappe.qb.from_(wo)
|
||||
.select(wo.name.as_("work_order"), wo.qty, wo.produced_qty, wo.production_item, wo.bom_no)
|
||||
.where((wo.produced_qty > wo.qty) & (wo.docstatus == 1))
|
||||
)
|
||||
|
||||
if filters.get("bom_no") and not filters.get("work_order"):
|
||||
cond += " and bom_no = '%s'" % filters.get("bom_no")
|
||||
query = query.where(wo.bom_no == filters.get("bom_no"))
|
||||
|
||||
if filters.get("work_order"):
|
||||
cond += " and name = '%s'" % filters.get("work_order")
|
||||
query = query.where(wo.name == filters.get("work_order"))
|
||||
|
||||
results = []
|
||||
for d in frappe.db.sql(
|
||||
""" select name as work_order, qty, produced_qty, production_item, bom_no
|
||||
from `tabWork Order` where produced_qty > qty and docstatus = 1 and {0}""".format(
|
||||
cond
|
||||
),
|
||||
as_dict=1,
|
||||
):
|
||||
for d in query.run(as_dict=True):
|
||||
results.append(d)
|
||||
|
||||
for data in frappe.get_all(
|
||||
@@ -95,16 +94,17 @@ def get_data(filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_work_orders(doctype, txt, searchfield, start, page_len, filters):
|
||||
cond = "1=1"
|
||||
if filters.get("bom_no"):
|
||||
cond += " and bom_no = '%s'" % filters.get("bom_no")
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select name from `tabWork Order`
|
||||
where name like %(name)s and {0} and produced_qty > qty and docstatus = 1
|
||||
order by name limit {1}, {2}""".format(
|
||||
cond, start, page_len
|
||||
),
|
||||
{"name": "%%%s%%" % txt},
|
||||
as_list=1,
|
||||
wo = frappe.qb.DocType("Work Order")
|
||||
query = (
|
||||
frappe.qb.from_(wo)
|
||||
.select(wo.name)
|
||||
.where((wo.name.like(f"{txt}%")) & (wo.produced_qty > wo.qty) & (wo.docstatus == 1))
|
||||
.orderby(wo.name)
|
||||
.limit(page_len)
|
||||
.offset(start)
|
||||
)
|
||||
|
||||
if filters.get("bom_no"):
|
||||
query = query.where(wo.bom_no == filters.get("bom_no"))
|
||||
|
||||
return query.run(as_list=True)
|
||||
|
||||
@@ -96,38 +96,39 @@ class ForecastingReport(ExponentialSmoothingForecast):
|
||||
value["avg"] = flt(sum(list_of_period_value)) / flt(sum(total_qty))
|
||||
|
||||
def get_data_for_forecast(self):
|
||||
cond = ""
|
||||
if self.filters.item_code:
|
||||
cond = " AND soi.item_code = %s" % (frappe.db.escape(self.filters.item_code))
|
||||
|
||||
warehouses = []
|
||||
if self.filters.warehouse:
|
||||
warehouses = get_child_warehouses(self.filters.warehouse)
|
||||
cond += " AND soi.warehouse in ({})".format(",".join(["%s"] * len(warehouses)))
|
||||
|
||||
input_data = [self.filters.from_date, self.filters.company]
|
||||
if warehouses:
|
||||
input_data.extend(warehouses)
|
||||
parent = frappe.qb.DocType(self.doctype)
|
||||
child = frappe.qb.DocType(self.child_doctype)
|
||||
|
||||
date_field = "posting_date" if self.doctype == "Delivery Note" else "transaction_date"
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
so.{date_field} as posting_date, soi.item_code, soi.warehouse,
|
||||
soi.item_name, soi.stock_qty as qty, soi.base_amount as amount
|
||||
FROM
|
||||
`tab{doc}` so, `tab{child_doc}` soi
|
||||
WHERE
|
||||
so.docstatus = 1 AND so.name = soi.parent AND
|
||||
so.{date_field} < %s AND so.company = %s {cond}
|
||||
""".format(
|
||||
doc=self.doctype, child_doc=self.child_doctype, date_field=date_field, cond=cond
|
||||
),
|
||||
tuple(input_data),
|
||||
as_dict=1,
|
||||
query = (
|
||||
frappe.qb.from_(parent)
|
||||
.from_(child)
|
||||
.select(
|
||||
parent[date_field].as_("posting_date"),
|
||||
child.item_code,
|
||||
child.warehouse,
|
||||
child.item_name,
|
||||
child.stock_qty.as_("qty"),
|
||||
child.base_amount.as_("amount"),
|
||||
)
|
||||
.where(
|
||||
(parent.docstatus == 1)
|
||||
& (parent.name == child.parent)
|
||||
& (parent[date_field] < self.filters.from_date)
|
||||
& (parent.company == self.filters.company)
|
||||
)
|
||||
)
|
||||
|
||||
if self.filters.item_code:
|
||||
query = query.where(child.item_code == self.filters.item_code)
|
||||
|
||||
if self.filters.warehouse:
|
||||
warehouses = get_child_warehouses(self.filters.warehouse) or []
|
||||
query = query.where(child.warehouse.isin(warehouses))
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
def prepare_final_data(self):
|
||||
self.data = []
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import Dict, List, Tuple
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
Filters = frappe._dict
|
||||
Row = frappe._dict
|
||||
@@ -14,15 +15,50 @@ QueryArgs = Dict[str, str]
|
||||
|
||||
|
||||
def execute(filters: Filters) -> Tuple[Columns, Data]:
|
||||
filters = frappe._dict(filters or {})
|
||||
columns = get_columns()
|
||||
data = get_data(filters)
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_data(filters: Filters) -> Data:
|
||||
query_args = get_query_args(filters)
|
||||
data = run_query(query_args)
|
||||
wo = frappe.qb.DocType("Work Order")
|
||||
se = frappe.qb.DocType("Stock Entry")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(wo)
|
||||
.inner_join(se)
|
||||
.on(wo.name == se.work_order)
|
||||
.select(
|
||||
wo.name,
|
||||
wo.status,
|
||||
wo.production_item,
|
||||
wo.qty,
|
||||
wo.produced_qty,
|
||||
wo.process_loss_qty,
|
||||
(wo.produced_qty - wo.process_loss_qty).as_("actual_produced_qty"),
|
||||
Sum(se.total_incoming_value).as_("total_fg_value"),
|
||||
Sum(se.total_outgoing_value).as_("total_rm_value"),
|
||||
)
|
||||
.where(
|
||||
(wo.process_loss_qty > 0)
|
||||
& (wo.company == filters.company)
|
||||
& (se.docstatus == 1)
|
||||
& (se.posting_date.between(filters.from_date, filters.to_date))
|
||||
)
|
||||
.groupby(se.work_order)
|
||||
)
|
||||
|
||||
if "item" in filters:
|
||||
query.where(wo.production_item == filters.item)
|
||||
|
||||
if "work_order" in filters:
|
||||
query.where(wo.name == filters.work_order)
|
||||
|
||||
data = query.run(as_dict=True)
|
||||
|
||||
update_data_with_total_pl_value(data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -67,54 +103,7 @@ def get_columns() -> Columns:
|
||||
]
|
||||
|
||||
|
||||
def get_query_args(filters: Filters) -> QueryArgs:
|
||||
query_args = {}
|
||||
query_args.update(filters)
|
||||
query_args.update(get_filter_conditions(filters))
|
||||
return query_args
|
||||
|
||||
|
||||
def run_query(query_args: QueryArgs) -> Data:
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
wo.name, wo.status, wo.production_item, wo.qty,
|
||||
wo.produced_qty, wo.process_loss_qty,
|
||||
(wo.produced_qty - wo.process_loss_qty) as actual_produced_qty,
|
||||
sum(se.total_incoming_value) as total_fg_value,
|
||||
sum(se.total_outgoing_value) as total_rm_value
|
||||
FROM
|
||||
`tabWork Order` wo INNER JOIN `tabStock Entry` se
|
||||
ON wo.name=se.work_order
|
||||
WHERE
|
||||
process_loss_qty > 0
|
||||
AND wo.company = %(company)s
|
||||
AND se.docstatus = 1
|
||||
AND se.posting_date BETWEEN %(from_date)s AND %(to_date)s
|
||||
{item_filter}
|
||||
{work_order_filter}
|
||||
GROUP BY
|
||||
se.work_order
|
||||
""".format(
|
||||
**query_args
|
||||
),
|
||||
query_args,
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
def update_data_with_total_pl_value(data: Data) -> None:
|
||||
for row in data:
|
||||
value_per_unit_fg = row["total_fg_value"] / row["actual_produced_qty"]
|
||||
row["total_pl_value"] = row["process_loss_qty"] * value_per_unit_fg
|
||||
|
||||
|
||||
def get_filter_conditions(filters: Filters) -> QueryArgs:
|
||||
filter_conditions = dict(item_filter="", work_order_filter="")
|
||||
if "item" in filters:
|
||||
production_item = filters.get("item")
|
||||
filter_conditions.update({"item_filter": f"AND wo.production_item='{production_item}'"})
|
||||
if "work_order" in filters:
|
||||
work_order_name = filters.get("work_order")
|
||||
filter_conditions.update({"work_order_filter": f"AND wo.name='{work_order_name}'"})
|
||||
return filter_conditions
|
||||
|
||||
@@ -4,42 +4,10 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from pypika import Order
|
||||
|
||||
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
|
||||
|
||||
# and bom_no is not null and bom_no !=''
|
||||
|
||||
mapper = {
|
||||
"Sales Order": {
|
||||
"fields": """ item_code as production_item, item_name as production_item_name, stock_uom,
|
||||
stock_qty as qty_to_manufacture, `tabSales Order Item`.parent as name, bom_no, warehouse,
|
||||
`tabSales Order Item`.delivery_date, `tabSales Order`.base_grand_total """,
|
||||
"filters": """`tabSales Order Item`.docstatus = 1 and stock_qty > produced_qty
|
||||
and `tabSales Order`.per_delivered < 100.0""",
|
||||
},
|
||||
"Material Request": {
|
||||
"fields": """ item_code as production_item, item_name as production_item_name, stock_uom,
|
||||
stock_qty as qty_to_manufacture, `tabMaterial Request Item`.parent as name, bom_no, warehouse,
|
||||
`tabMaterial Request Item`.schedule_date """,
|
||||
"filters": """`tabMaterial Request`.docstatus = 1 and `tabMaterial Request`.per_ordered < 100
|
||||
and `tabMaterial Request`.material_request_type = 'Manufacture' """,
|
||||
},
|
||||
"Work Order": {
|
||||
"fields": """ production_item, item_name as production_item_name, planned_start_date,
|
||||
stock_uom, qty as qty_to_manufacture, name, bom_no, fg_warehouse as warehouse """,
|
||||
"filters": "docstatus = 1 and status not in ('Completed', 'Stopped')",
|
||||
},
|
||||
}
|
||||
|
||||
order_mapper = {
|
||||
"Sales Order": {
|
||||
"Delivery Date": "`tabSales Order Item`.delivery_date asc",
|
||||
"Total Amount": "`tabSales Order`.base_grand_total desc",
|
||||
},
|
||||
"Material Request": {"Required Date": "`tabMaterial Request Item`.schedule_date asc"},
|
||||
"Work Order": {"Planned Start Date": "planned_start_date asc"},
|
||||
}
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
return ProductionPlanReport(filters).execute_report()
|
||||
@@ -63,40 +31,78 @@ class ProductionPlanReport(object):
|
||||
return self.columns, self.data
|
||||
|
||||
def get_open_orders(self):
|
||||
doctype = (
|
||||
"`tabWork Order`"
|
||||
if self.filters.based_on == "Work Order"
|
||||
else "`tab{doc}`, `tab{doc} Item`".format(doc=self.filters.based_on)
|
||||
)
|
||||
doctype, order_by = self.filters.based_on, self.filters.order_by
|
||||
|
||||
filters = mapper.get(self.filters.based_on)["filters"]
|
||||
filters = self.prepare_other_conditions(filters, self.filters.based_on)
|
||||
order_by = " ORDER BY %s" % (order_mapper[self.filters.based_on][self.filters.order_by])
|
||||
parent = frappe.qb.DocType(doctype)
|
||||
query = None
|
||||
|
||||
self.orders = frappe.db.sql(
|
||||
""" SELECT {fields} from {doctype}
|
||||
WHERE {filters} {order_by}""".format(
|
||||
doctype=doctype,
|
||||
filters=filters,
|
||||
order_by=order_by,
|
||||
fields=mapper.get(self.filters.based_on)["fields"],
|
||||
),
|
||||
tuple(self.filters.docnames),
|
||||
as_dict=1,
|
||||
)
|
||||
if doctype == "Work Order":
|
||||
query = (
|
||||
frappe.qb.from_(parent)
|
||||
.select(
|
||||
parent.production_item,
|
||||
parent.item_name.as_("production_item_name"),
|
||||
parent.planned_start_date,
|
||||
parent.stock_uom,
|
||||
parent.qty.as_("qty_to_manufacture"),
|
||||
parent.name,
|
||||
parent.bom_no,
|
||||
parent.fg_warehouse.as_("warehouse"),
|
||||
)
|
||||
.where(parent.status.notin(["Completed", "Stopped"]))
|
||||
)
|
||||
|
||||
def prepare_other_conditions(self, filters, doctype):
|
||||
if self.filters.docnames:
|
||||
field = "name" if doctype == "Work Order" else "`tab{} Item`.parent".format(doctype)
|
||||
filters += " and %s in (%s)" % (field, ",".join(["%s"] * len(self.filters.docnames)))
|
||||
if order_by == "Planned Start Date":
|
||||
query = query.orderby(parent.planned_start_date, order=Order.asc)
|
||||
|
||||
if doctype != "Work Order":
|
||||
filters += " and `tab{doc}`.name = `tab{doc} Item`.parent".format(doc=doctype)
|
||||
if self.filters.docnames:
|
||||
query = query.where(parent.name.isin(self.filters.docnames))
|
||||
|
||||
else:
|
||||
child = frappe.qb.DocType(f"{doctype} Item")
|
||||
query = (
|
||||
frappe.qb.from_(parent)
|
||||
.from_(child)
|
||||
.select(
|
||||
child.bom_no,
|
||||
child.stock_uom,
|
||||
child.warehouse,
|
||||
child.parent.as_("name"),
|
||||
child.item_code.as_("production_item"),
|
||||
child.stock_qty.as_("qty_to_manufacture"),
|
||||
child.item_name.as_("production_item_name"),
|
||||
)
|
||||
.where(parent.name == child.parent)
|
||||
)
|
||||
|
||||
if self.filters.docnames:
|
||||
query = query.where(child.parent.isin(self.filters.docnames))
|
||||
|
||||
if doctype == "Sales Order":
|
||||
query = query.select(
|
||||
child.delivery_date,
|
||||
parent.base_grand_total,
|
||||
).where((child.stock_qty > child.produced_qty) & (parent.per_delivered < 100.0))
|
||||
|
||||
if order_by == "Delivery Date":
|
||||
query = query.orderby(child.delivery_date, order=Order.asc)
|
||||
elif order_by == "Total Amount":
|
||||
query = query.orderby(parent.base_grand_total, order=Order.desc)
|
||||
|
||||
elif doctype == "Material Request":
|
||||
query = query.select(child.schedule_date,).where(
|
||||
(parent.per_ordered < 100) & (parent.material_request_type == "Manufacture")
|
||||
)
|
||||
|
||||
if order_by == "Required Date":
|
||||
query = query.orderby(child.schedule_date, order=Order.asc)
|
||||
|
||||
query = query.where(parent.docstatus == 1)
|
||||
|
||||
if self.filters.company:
|
||||
filters += " and `tab%s`.company = %s" % (doctype, frappe.db.escape(self.filters.company))
|
||||
query = query.where(parent.company == self.filters.company)
|
||||
|
||||
return filters
|
||||
self.orders = query.run(as_dict=True)
|
||||
|
||||
def get_raw_materials(self):
|
||||
if not self.orders:
|
||||
@@ -134,29 +140,29 @@ class ProductionPlanReport(object):
|
||||
|
||||
bom_nos.append(bom_no)
|
||||
|
||||
bom_doctype = (
|
||||
bom_item_doctype = (
|
||||
"BOM Explosion Item" if self.filters.include_subassembly_raw_materials else "BOM Item"
|
||||
)
|
||||
|
||||
qty_field = (
|
||||
"qty_consumed_per_unit"
|
||||
if self.filters.include_subassembly_raw_materials
|
||||
else "(bom_item.qty / bom.quantity)"
|
||||
)
|
||||
bom = frappe.qb.DocType("BOM")
|
||||
bom_item = frappe.qb.DocType(bom_item_doctype)
|
||||
|
||||
raw_materials = frappe.db.sql(
|
||||
""" SELECT bom_item.parent, bom_item.item_code,
|
||||
bom_item.item_name as raw_material_name, {0} as required_qty_per_unit
|
||||
FROM
|
||||
`tabBOM` as bom, `tab{1}` as bom_item
|
||||
WHERE
|
||||
bom_item.parent in ({2}) and bom_item.parent = bom.name and bom.docstatus = 1
|
||||
""".format(
|
||||
qty_field, bom_doctype, ",".join(["%s"] * len(bom_nos))
|
||||
),
|
||||
tuple(bom_nos),
|
||||
as_dict=1,
|
||||
)
|
||||
if self.filters.include_subassembly_raw_materials:
|
||||
qty_field = bom_item.qty_consumed_per_unit
|
||||
else:
|
||||
qty_field = bom_item.qty / bom.quantity
|
||||
|
||||
raw_materials = (
|
||||
frappe.qb.from_(bom)
|
||||
.from_(bom_item)
|
||||
.select(
|
||||
bom_item.parent,
|
||||
bom_item.item_code,
|
||||
bom_item.item_name.as_("raw_material_name"),
|
||||
qty_field.as_("required_qty_per_unit"),
|
||||
)
|
||||
.where((bom_item.parent.isin(bom_nos)) & (bom_item.parent == bom.name) & (bom.docstatus == 1))
|
||||
).run(as_dict=True)
|
||||
|
||||
if not raw_materials:
|
||||
return
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder.functions import IfNull
|
||||
from frappe.utils import cint
|
||||
|
||||
|
||||
@@ -16,70 +17,70 @@ def execute(filters=None):
|
||||
def get_item_list(wo_list, filters):
|
||||
out = []
|
||||
|
||||
# Add a row for each item/qty
|
||||
for wo_details in wo_list:
|
||||
desc = frappe.db.get_value("BOM", wo_details.bom_no, "description")
|
||||
if wo_list:
|
||||
bin = frappe.qb.DocType("Bin")
|
||||
bom = frappe.qb.DocType("BOM")
|
||||
bom_item = frappe.qb.DocType("BOM Item")
|
||||
|
||||
for wo_item_details in frappe.db.get_values(
|
||||
"Work Order Item", {"parent": wo_details.name}, ["item_code", "source_warehouse"], as_dict=1
|
||||
):
|
||||
# Add a row for each item/qty
|
||||
for wo_details in wo_list:
|
||||
desc = frappe.db.get_value("BOM", wo_details.bom_no, "description")
|
||||
|
||||
item_list = frappe.db.sql(
|
||||
"""SELECT
|
||||
bom_item.item_code as item_code,
|
||||
ifnull(ledger.actual_qty*bom.quantity/bom_item.stock_qty,0) as build_qty
|
||||
FROM
|
||||
`tabBOM` as bom, `tabBOM Item` AS bom_item
|
||||
LEFT JOIN `tabBin` AS ledger
|
||||
ON bom_item.item_code = ledger.item_code
|
||||
AND ledger.warehouse = ifnull(%(warehouse)s,%(filterhouse)s)
|
||||
WHERE
|
||||
bom.name = bom_item.parent
|
||||
and bom_item.item_code = %(item_code)s
|
||||
and bom.name = %(bom)s
|
||||
GROUP BY
|
||||
bom_item.item_code""",
|
||||
{
|
||||
"bom": wo_details.bom_no,
|
||||
"warehouse": wo_item_details.source_warehouse,
|
||||
"filterhouse": filters.warehouse,
|
||||
"item_code": wo_item_details.item_code,
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
for wo_item_details in frappe.db.get_values(
|
||||
"Work Order Item", {"parent": wo_details.name}, ["item_code", "source_warehouse"], as_dict=1
|
||||
):
|
||||
item_list = (
|
||||
frappe.qb.from_(bom)
|
||||
.from_(bom_item)
|
||||
.left_join(bin)
|
||||
.on(
|
||||
(bom_item.item_code == bin.item_code)
|
||||
& (bin.warehouse == IfNull(wo_item_details.source_warehouse, filters.warehouse))
|
||||
)
|
||||
.select(
|
||||
bom_item.item_code.as_("item_code"),
|
||||
IfNull(bin.actual_qty * bom.quantity / bom_item.stock_qty, 0).as_("build_qty"),
|
||||
)
|
||||
.where(
|
||||
(bom.name == bom_item.parent)
|
||||
& (bom_item.item_code == wo_item_details.item_code)
|
||||
& (bom.name == wo_details.bom_no)
|
||||
)
|
||||
.groupby(bom_item.item_code)
|
||||
).run(as_dict=1)
|
||||
|
||||
stock_qty = 0
|
||||
count = 0
|
||||
buildable_qty = wo_details.qty
|
||||
for item in item_list:
|
||||
count = count + 1
|
||||
if item.build_qty >= (wo_details.qty - wo_details.produced_qty):
|
||||
stock_qty = stock_qty + 1
|
||||
elif buildable_qty >= item.build_qty:
|
||||
buildable_qty = item.build_qty
|
||||
stock_qty = 0
|
||||
count = 0
|
||||
buildable_qty = wo_details.qty
|
||||
for item in item_list:
|
||||
count = count + 1
|
||||
if item.build_qty >= (wo_details.qty - wo_details.produced_qty):
|
||||
stock_qty = stock_qty + 1
|
||||
elif buildable_qty >= item.build_qty:
|
||||
buildable_qty = item.build_qty
|
||||
|
||||
if count == stock_qty:
|
||||
build = "Y"
|
||||
else:
|
||||
build = "N"
|
||||
if count == stock_qty:
|
||||
build = "Y"
|
||||
else:
|
||||
build = "N"
|
||||
|
||||
row = frappe._dict(
|
||||
{
|
||||
"work_order": wo_details.name,
|
||||
"status": wo_details.status,
|
||||
"req_items": cint(count),
|
||||
"instock": stock_qty,
|
||||
"description": desc,
|
||||
"source_warehouse": wo_item_details.source_warehouse,
|
||||
"item_code": wo_item_details.item_code,
|
||||
"bom_no": wo_details.bom_no,
|
||||
"qty": wo_details.qty,
|
||||
"buildable_qty": buildable_qty,
|
||||
"ready_to_build": build,
|
||||
}
|
||||
)
|
||||
row = frappe._dict(
|
||||
{
|
||||
"work_order": wo_details.name,
|
||||
"status": wo_details.status,
|
||||
"req_items": cint(count),
|
||||
"instock": stock_qty,
|
||||
"description": desc,
|
||||
"source_warehouse": wo_item_details.source_warehouse,
|
||||
"item_code": wo_item_details.item_code,
|
||||
"bom_no": wo_details.bom_no,
|
||||
"qty": wo_details.qty,
|
||||
"buildable_qty": buildable_qty,
|
||||
"ready_to_build": build,
|
||||
}
|
||||
)
|
||||
|
||||
out.append(row)
|
||||
out.append(row)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@@ -374,3 +374,4 @@ erpnext.patches.v13_0.reset_corrupt_defaults
|
||||
erpnext.patches.v13_0.show_hr_payroll_deprecation_warning
|
||||
erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair
|
||||
execute:frappe.db.set_value("Naming Series", "Naming Series", {"select_doc_for_series": "", "set_options": "", "prefix": "", "current_value": 0, "user_must_always_select": 0})
|
||||
erpnext.patches.v13_0.update_schedule_type_in_loans
|
||||
@@ -100,6 +100,7 @@ def execute():
|
||||
"mode_of_payment": loan.mode_of_payment,
|
||||
"loan_account": loan.loan_account,
|
||||
"payment_account": loan.payment_account,
|
||||
"disbursement_account": loan.payment_account,
|
||||
"interest_income_account": loan.interest_income_account,
|
||||
"penalty_income_account": loan.penalty_income_account,
|
||||
},
|
||||
@@ -190,6 +191,7 @@ def create_loan_type(loan, loan_type_name, penalty_account):
|
||||
loan_type_doc.company = loan.company
|
||||
loan_type_doc.mode_of_payment = loan.mode_of_payment
|
||||
loan_type_doc.payment_account = loan.payment_account
|
||||
loan_type_doc.disbursement_account = loan.payment_account
|
||||
loan_type_doc.loan_account = loan.loan_account
|
||||
loan_type_doc.interest_income_account = loan.interest_income_account
|
||||
loan_type_doc.penalty_income_account = penalty_account
|
||||
|
||||
17
erpnext/patches/v13_0/update_schedule_type_in_loans.py
Normal file
17
erpnext/patches/v13_0/update_schedule_type_in_loans.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("loan_management", "doctype", "loan")
|
||||
frappe.reload_doc("loan_management", "doctype", "loan_type")
|
||||
|
||||
loan = frappe.qb.DocType("Loan")
|
||||
loan_type = frappe.qb.DocType("Loan Type")
|
||||
|
||||
frappe.qb.update(loan_type).set(
|
||||
loan_type.repayment_schedule_type, "Monthly as per repayment start date"
|
||||
).where(loan_type.is_term_loan == 1).run()
|
||||
|
||||
frappe.qb.update(loan).set(
|
||||
loan.repayment_schedule_type, "Monthly as per repayment start date"
|
||||
).where(loan.is_term_loan == 1).run()
|
||||
@@ -295,6 +295,7 @@ class TestPayrollEntry(FrappeTestCase):
|
||||
loan_account="Loan Account - _TC",
|
||||
interest_income_account="Interest Income Account - _TC",
|
||||
penalty_income_account="Penalty Income Account - _TC",
|
||||
repayment_schedule_type="Monthly as per repayment start date",
|
||||
)
|
||||
|
||||
loan = create_loan(
|
||||
|
||||
@@ -545,6 +545,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
if(!this.validate_company_and_party()) {
|
||||
this.frm.fields_dict["items"].grid.grid_rows[item.idx - 1].remove();
|
||||
} else {
|
||||
item.pricing_rules = ''
|
||||
return this.frm.call({
|
||||
method: "erpnext.stock.get_item_details.get_item_details",
|
||||
child: item,
|
||||
@@ -1160,6 +1161,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
uom: function(doc, cdt, cdn) {
|
||||
var me = this;
|
||||
var item = frappe.get_doc(cdt, cdn);
|
||||
item.pricing_rules = ''
|
||||
if(item.item_code && item.uom) {
|
||||
return this.frm.call({
|
||||
method: "erpnext.stock.get_item_details.get_conversion_factor",
|
||||
@@ -1236,6 +1238,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
|
||||
qty: function(doc, cdt, cdn) {
|
||||
let item = frappe.get_doc(cdt, cdn);
|
||||
item.pricing_rules = ''
|
||||
this.conversion_factor(doc, cdt, cdn, true);
|
||||
this.calculate_stock_uom_rate(doc, cdt, cdn);
|
||||
this.apply_pricing_rule(item, true);
|
||||
|
||||
@@ -242,20 +242,29 @@ erpnext.utils.set_taxes = function(frm, triggered_from_field) {
|
||||
});
|
||||
};
|
||||
|
||||
erpnext.utils.get_contact_details = function(frm) {
|
||||
erpnext.utils.get_contact_details = function (frm) {
|
||||
if (frm.updating_party_details) return;
|
||||
|
||||
if (frm.doc["contact_person"]) {
|
||||
frappe.call({
|
||||
method: "frappe.contacts.doctype.contact.contact.get_contact_details",
|
||||
args: {contact: frm.doc.contact_person },
|
||||
callback: function(r) {
|
||||
if (r.message)
|
||||
frm.set_value(r.message);
|
||||
}
|
||||
})
|
||||
args: { contact: frm.doc.contact_person },
|
||||
callback: function (r) {
|
||||
if (r.message) frm.set_value(r.message);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
frm.set_value({
|
||||
contact_person: "",
|
||||
contact_display: "",
|
||||
contact_email: "",
|
||||
contact_mobile: "",
|
||||
contact_phone: "",
|
||||
contact_designation: "",
|
||||
contact_department: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
erpnext.utils.validate_mandatory = function(frm, label, value, trigger_on) {
|
||||
if (!value) {
|
||||
|
||||
@@ -894,6 +894,7 @@ class GSPConnector:
|
||||
return self.e_invoice_settings.auth_token
|
||||
|
||||
def make_request(self, request_type, url, headers=None, data=None):
|
||||
res = None
|
||||
try:
|
||||
if request_type == "post":
|
||||
res = make_post_request(url, headers=headers, data=data)
|
||||
|
||||
@@ -47,6 +47,12 @@ erpnext.setup_auto_gst_taxation = (doctype) => {
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
reverse_charge: function(frm) {
|
||||
if (frm.doc.reverse_charge == "Y") {
|
||||
frm.set_value('eligibility_for_itc', 'ITC on Reverse Charge');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -257,9 +257,16 @@ def get_regional_address_details(party_details, doctype, company):
|
||||
|
||||
update_party_details(party_details, doctype)
|
||||
|
||||
customer_gst_category = frappe.get_value(
|
||||
"Customer", party_details.customer, ["gst_category", "export_type"]
|
||||
)
|
||||
|
||||
party_details.place_of_supply = get_place_of_supply(party_details, doctype)
|
||||
|
||||
if is_internal_transfer(party_details, doctype):
|
||||
if is_internal_transfer(party_details, doctype) or customer_gst_category == (
|
||||
"SEZ",
|
||||
"Without Payment of Tax",
|
||||
):
|
||||
party_details.taxes_and_charges = ""
|
||||
party_details.taxes = []
|
||||
return party_details
|
||||
@@ -603,6 +610,10 @@ def get_ewb_data(dt, dn):
|
||||
|
||||
data = get_address_details(data, doc, company_address, billing_address, dispatch_address)
|
||||
|
||||
if is_intrastate_transfer_eway_bill(data):
|
||||
data.docType = "CHL"
|
||||
data.subSupplyType = 8
|
||||
|
||||
data.itemList = []
|
||||
data.totalValue = doc.total
|
||||
|
||||
@@ -645,6 +656,10 @@ def get_ewb_data(dt, dn):
|
||||
return data
|
||||
|
||||
|
||||
def is_intrastate_transfer_eway_bill(data):
|
||||
return data.fromGstin == data.toGstin
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def generate_ewb_json(dt, dn):
|
||||
dn = json.loads(dn)
|
||||
@@ -968,8 +983,6 @@ def validate_reverse_charge_transaction(doc, method):
|
||||
|
||||
frappe.throw(msg)
|
||||
|
||||
doc.eligibility_for_itc = "ITC on Reverse Charge"
|
||||
|
||||
|
||||
def update_itc_availed_fields(doc, method):
|
||||
country = frappe.get_cached_value("Company", doc.company, "country")
|
||||
|
||||
@@ -15,12 +15,6 @@ filters = filters.concat({
|
||||
"placeholder":"Company GSTIN",
|
||||
"options": [""],
|
||||
"width": "80"
|
||||
}, {
|
||||
"fieldname":"invoice_type",
|
||||
"label": __("Invoice Type"),
|
||||
"fieldtype": "Select",
|
||||
"placeholder":"Invoice Type",
|
||||
"options": ["", "Regular", "SEZ", "Export", "Deemed Export"]
|
||||
});
|
||||
|
||||
// Handle company on change
|
||||
|
||||
@@ -5,31 +5,48 @@
|
||||
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import _execute
|
||||
|
||||
|
||||
def get_conditions(filters, additional_query_columns):
|
||||
conditions = ""
|
||||
|
||||
for opts in additional_query_columns:
|
||||
if filters.get(opts):
|
||||
conditions += f" and {opts}=%({opts})s"
|
||||
|
||||
return conditions
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
additional_table_columns = [
|
||||
dict(fieldtype="Data", label="Customer GSTIN", fieldname="customer_gstin", width=120),
|
||||
dict(
|
||||
fieldtype="Data", label="Billing Address GSTIN", fieldname="billing_address_gstin", width=140
|
||||
),
|
||||
dict(fieldtype="Data", label="Company GSTIN", fieldname="company_gstin", width=120),
|
||||
dict(fieldtype="Data", label="Place of Supply", fieldname="place_of_supply", width=120),
|
||||
dict(fieldtype="Data", label="Reverse Charge", fieldname="reverse_charge", width=120),
|
||||
dict(fieldtype="Data", label="GST Category", fieldname="gst_category", width=120),
|
||||
dict(fieldtype="Data", label="Export Type", fieldname="export_type", width=120),
|
||||
dict(fieldtype="Data", label="E-Commerce GSTIN", fieldname="ecommerce_gstin", width=130),
|
||||
dict(fieldtype="Data", label="HSN Code", fieldname="gst_hsn_code", width=120),
|
||||
]
|
||||
|
||||
additional_query_columns = [
|
||||
"customer_gstin",
|
||||
"billing_address_gstin",
|
||||
"company_gstin",
|
||||
"place_of_supply",
|
||||
"reverse_charge",
|
||||
"gst_category",
|
||||
"export_type",
|
||||
"ecommerce_gstin",
|
||||
"gst_hsn_code",
|
||||
]
|
||||
|
||||
additional_conditions = get_conditions(filters, additional_query_columns)
|
||||
|
||||
return _execute(
|
||||
filters,
|
||||
additional_table_columns=[
|
||||
dict(fieldtype="Data", label="Customer GSTIN", fieldname="customer_gstin", width=120),
|
||||
dict(
|
||||
fieldtype="Data", label="Billing Address GSTIN", fieldname="billing_address_gstin", width=140
|
||||
),
|
||||
dict(fieldtype="Data", label="Company GSTIN", fieldname="company_gstin", width=120),
|
||||
dict(fieldtype="Data", label="Place of Supply", fieldname="place_of_supply", width=120),
|
||||
dict(fieldtype="Data", label="Reverse Charge", fieldname="reverse_charge", width=120),
|
||||
dict(fieldtype="Data", label="GST Category", fieldname="gst_category", width=120),
|
||||
dict(fieldtype="Data", label="Export Type", fieldname="export_type", width=120),
|
||||
dict(fieldtype="Data", label="E-Commerce GSTIN", fieldname="ecommerce_gstin", width=130),
|
||||
dict(fieldtype="Data", label="HSN Code", fieldname="gst_hsn_code", width=120),
|
||||
],
|
||||
additional_query_columns=[
|
||||
"customer_gstin",
|
||||
"billing_address_gstin",
|
||||
"company_gstin",
|
||||
"place_of_supply",
|
||||
"reverse_charge",
|
||||
"gst_category",
|
||||
"export_type",
|
||||
"ecommerce_gstin",
|
||||
"gst_hsn_code",
|
||||
],
|
||||
additional_table_columns=additional_table_columns,
|
||||
additional_query_columns=additional_query_columns,
|
||||
additional_conditions=additional_conditions,
|
||||
)
|
||||
|
||||
@@ -83,11 +83,12 @@ erpnext.selling.QuotationController = erpnext.selling.SellingController.extend({
|
||||
}
|
||||
}
|
||||
|
||||
if(doc.docstatus == 1 && !(['Lost', 'Ordered']).includes(doc.status)) {
|
||||
if(!doc.valid_till || frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) {
|
||||
cur_frm.add_custom_button(__('Sales Order'),
|
||||
cur_frm.cscript['Make Sales Order'], __('Create'));
|
||||
}
|
||||
if (doc.docstatus == 1 && !["Lost", "Ordered"].includes(doc.status)) {
|
||||
this.frm.add_custom_button(
|
||||
__("Sales Order"),
|
||||
this.frm.cscript["Make Sales Order"],
|
||||
__("Create")
|
||||
);
|
||||
|
||||
if(doc.status!=="Ordered") {
|
||||
this.frm.add_custom_button(__('Set as Lost'), () => {
|
||||
|
||||
@@ -461,7 +461,6 @@
|
||||
"fieldname": "ignore_pricing_rule",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Pricing Rule",
|
||||
"no_copy": 1,
|
||||
"permlevel": 1,
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
@@ -1174,7 +1173,7 @@
|
||||
"idx": 82,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-06-15 20:35:32.635804",
|
||||
"modified": "2022-09-16 17:44:43.221804",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Quotation",
|
||||
|
||||
@@ -95,6 +95,11 @@ frappe.ui.form.on("Sales Order", {
|
||||
return query;
|
||||
});
|
||||
|
||||
// On cancel and amending a sales order with advance payment, reset advance paid amount
|
||||
if (frm.is_new()) {
|
||||
frm.set_value("advance_paid", 0)
|
||||
}
|
||||
|
||||
frm.ignore_doctypes_on_cancel_all = ['Purchase Order'];
|
||||
},
|
||||
|
||||
|
||||
@@ -544,7 +544,6 @@
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Ignore Pricing Rule",
|
||||
"no_copy": 1,
|
||||
"permlevel": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
@@ -1549,7 +1548,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-06-10 03:52:22.212953",
|
||||
"modified": "2022-09-16 17:43:57.007441",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order",
|
||||
|
||||
@@ -19,6 +19,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
update_linked_doc,
|
||||
validate_inter_company_party,
|
||||
)
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
from erpnext.manufacturing.doctype.production_plan.production_plan import (
|
||||
get_items_for_material_requests,
|
||||
@@ -797,6 +798,8 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
|
||||
if source.loyalty_points and source.order_type == "Shopping Cart":
|
||||
target.redeem_loyalty_points = 1
|
||||
|
||||
target.debit_to = get_party_account("Customer", source.customer, source.company)
|
||||
|
||||
def update_item(source, target, source_parent):
|
||||
target.amount = flt(source.amount) - flt(source.billed_amt)
|
||||
target.base_amount = target.amount * flt(source_parent.conversion_rate)
|
||||
|
||||
@@ -67,7 +67,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
{
|
||||
fieldtype: 'Link', label: __('POS Profile'),
|
||||
options: 'POS Profile', fieldname: 'pos_profile', reqd: 1,
|
||||
get_query: () => pos_profile_query,
|
||||
get_query: () => pos_profile_query(),
|
||||
onchange: () => fetch_pos_payment_methods()
|
||||
},
|
||||
{
|
||||
@@ -101,9 +101,11 @@ erpnext.PointOfSale.Controller = class {
|
||||
primary_action_label: __('Submit')
|
||||
});
|
||||
dialog.show();
|
||||
const pos_profile_query = {
|
||||
query: 'erpnext.accounts.doctype.pos_profile.pos_profile.pos_profile_query',
|
||||
filters: { company: dialog.fields_dict.company.get_value() }
|
||||
const pos_profile_query = () => {
|
||||
return {
|
||||
query: 'erpnext.accounts.doctype.pos_profile.pos_profile.pos_profile_query',
|
||||
filters: { company: dialog.fields_dict.company.get_value() }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -660,7 +662,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else if (available_qty < qty_needed) {
|
||||
} else if (is_stock_item && available_qty < qty_needed) {
|
||||
frappe.throw({
|
||||
message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]),
|
||||
indicator: 'orange'
|
||||
@@ -694,7 +696,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
callback(res) {
|
||||
if (!me.item_stock_map[item_code])
|
||||
me.item_stock_map[item_code] = {};
|
||||
me.item_stock_map[item_code][warehouse] = res.message[0];
|
||||
me.item_stock_map[item_code][warehouse] = res.message;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -242,13 +242,14 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
if (this.value) {
|
||||
me.events.form_updated(me.current_item, 'warehouse', this.value).then(() => {
|
||||
me.item_stock_map = me.events.get_item_stock_map();
|
||||
const available_qty = me.item_stock_map[me.item_row.item_code][this.value];
|
||||
const available_qty = me.item_stock_map[me.item_row.item_code][this.value][0];
|
||||
const is_stock_item = Boolean(me.item_stock_map[me.item_row.item_code][this.value][1]);
|
||||
if (available_qty === undefined) {
|
||||
me.events.get_available_stock(me.item_row.item_code, this.value).then(() => {
|
||||
// item stock map is updated now reset warehouse
|
||||
me.warehouse_control.set_value(this.value);
|
||||
})
|
||||
} else if (available_qty === 0) {
|
||||
} else if (available_qty === 0 && is_stock_item) {
|
||||
me.warehouse_control.set_value('');
|
||||
const bold_item_code = me.item_row.item_code.bold();
|
||||
const bold_warehouse = this.value.bold();
|
||||
|
||||
@@ -1,13 +1,71 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.ui.form.on('Brand', {
|
||||
setup: (frm) => {
|
||||
frm.fields_dict["brand_defaults"].grid.get_field("default_warehouse").get_query = function(doc, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
return {
|
||||
filters: { company: row.company }
|
||||
}
|
||||
}
|
||||
|
||||
frm.fields_dict["brand_defaults"].grid.get_field("default_discount_account").get_query = function(doc, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
return {
|
||||
filters: {
|
||||
'report_type': 'Profit and Loss',
|
||||
'company': row.company,
|
||||
"is_group": 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
//--------- ONLOAD -------------
|
||||
cur_frm.cscript.onload = function(doc, cdt, cdn) {
|
||||
frm.fields_dict["brand_defaults"].grid.get_field("buying_cost_center").get_query = function(doc, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
return {
|
||||
filters: {
|
||||
"is_group": 0,
|
||||
"company": row.company
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
frm.fields_dict["brand_defaults"].grid.get_field("expense_account").get_query = function(doc, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_expense_account",
|
||||
filters: { company: row.company }
|
||||
}
|
||||
}
|
||||
|
||||
cur_frm.cscript.refresh = function(doc, cdt, cdn) {
|
||||
frm.fields_dict["brand_defaults"].grid.get_field("default_provisional_account").get_query = function(doc, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
return {
|
||||
filters: {
|
||||
"company": row.company,
|
||||
"root_type": ["in", ["Liability", "Asset"]],
|
||||
"is_group": 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
frm.fields_dict["brand_defaults"].grid.get_field("selling_cost_center").get_query = function(doc, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
return {
|
||||
filters: {
|
||||
"is_group": 0,
|
||||
"company": row.company
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
frm.fields_dict["brand_defaults"].grid.get_field("income_account").get_query = function(doc, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_income_account",
|
||||
filters: { company: row.company }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,327 +0,0 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
# mappings for table dumps
|
||||
# "remember to add indexes!"
|
||||
|
||||
data_map = {
|
||||
"Company": {"columns": ["name"], "conditions": ["docstatus < 2"]},
|
||||
"Fiscal Year": {
|
||||
"columns": ["name", "year_start_date", "year_end_date"],
|
||||
"conditions": ["docstatus < 2"],
|
||||
},
|
||||
# Accounts
|
||||
"Account": {
|
||||
"columns": ["name", "parent_account", "lft", "rgt", "report_type", "company", "is_group"],
|
||||
"conditions": ["docstatus < 2"],
|
||||
"order_by": "lft",
|
||||
"links": {
|
||||
"company": ["Company", "name"],
|
||||
},
|
||||
},
|
||||
"Cost Center": {
|
||||
"columns": ["name", "lft", "rgt"],
|
||||
"conditions": ["docstatus < 2"],
|
||||
"order_by": "lft",
|
||||
},
|
||||
"GL Entry": {
|
||||
"columns": [
|
||||
"name",
|
||||
"account",
|
||||
"posting_date",
|
||||
"cost_center",
|
||||
"debit",
|
||||
"credit",
|
||||
"is_opening",
|
||||
"company",
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
"remarks",
|
||||
],
|
||||
"order_by": "posting_date, account",
|
||||
"links": {
|
||||
"account": ["Account", "name"],
|
||||
"company": ["Company", "name"],
|
||||
"cost_center": ["Cost Center", "name"],
|
||||
},
|
||||
},
|
||||
# Stock
|
||||
"Item": {
|
||||
"columns": [
|
||||
"name",
|
||||
"if(item_name=name, '', item_name) as item_name",
|
||||
"description",
|
||||
"item_group as parent_item_group",
|
||||
"stock_uom",
|
||||
"brand",
|
||||
"valuation_method",
|
||||
],
|
||||
# "conditions": ["docstatus < 2"],
|
||||
"order_by": "name",
|
||||
"links": {"parent_item_group": ["Item Group", "name"], "brand": ["Brand", "name"]},
|
||||
},
|
||||
"Item Group": {
|
||||
"columns": ["name", "parent_item_group"],
|
||||
# "conditions": ["docstatus < 2"],
|
||||
"order_by": "lft",
|
||||
},
|
||||
"Brand": {"columns": ["name"], "conditions": ["docstatus < 2"], "order_by": "name"},
|
||||
"Project": {"columns": ["name"], "conditions": ["docstatus < 2"], "order_by": "name"},
|
||||
"Warehouse": {"columns": ["name"], "conditions": ["docstatus < 2"], "order_by": "name"},
|
||||
"Stock Ledger Entry": {
|
||||
"columns": [
|
||||
"name",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"item_code",
|
||||
"warehouse",
|
||||
"actual_qty as qty",
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
"project",
|
||||
"incoming_rate as incoming_rate",
|
||||
"stock_uom",
|
||||
"serial_no",
|
||||
"qty_after_transaction",
|
||||
"valuation_rate",
|
||||
],
|
||||
"order_by": "posting_date, posting_time, creation",
|
||||
"links": {
|
||||
"item_code": ["Item", "name"],
|
||||
"warehouse": ["Warehouse", "name"],
|
||||
"project": ["Project", "name"],
|
||||
},
|
||||
"force_index": "posting_sort_index",
|
||||
},
|
||||
"Serial No": {
|
||||
"columns": ["name", "purchase_rate as incoming_rate"],
|
||||
"conditions": ["docstatus < 2"],
|
||||
"order_by": "name",
|
||||
},
|
||||
"Stock Entry": {
|
||||
"columns": ["name", "purpose"],
|
||||
"conditions": ["docstatus=1"],
|
||||
"order_by": "posting_date, posting_time, name",
|
||||
},
|
||||
"Material Request Item": {
|
||||
"columns": ["item.name as name", "item_code", "warehouse", "(qty - ordered_qty) as qty"],
|
||||
"from": "`tabMaterial Request Item` item, `tabMaterial Request` main",
|
||||
"conditions": [
|
||||
"item.parent = main.name",
|
||||
"main.docstatus=1",
|
||||
"main.status != 'Stopped'",
|
||||
"ifnull(warehouse, '')!=''",
|
||||
"qty > ordered_qty",
|
||||
],
|
||||
"links": {"item_code": ["Item", "name"], "warehouse": ["Warehouse", "name"]},
|
||||
},
|
||||
"Purchase Order Item": {
|
||||
"columns": [
|
||||
"item.name as name",
|
||||
"item_code",
|
||||
"warehouse",
|
||||
"(qty - received_qty)*conversion_factor as qty",
|
||||
],
|
||||
"from": "`tabPurchase Order Item` item, `tabPurchase Order` main",
|
||||
"conditions": [
|
||||
"item.parent = main.name",
|
||||
"main.docstatus=1",
|
||||
"main.status != 'Stopped'",
|
||||
"ifnull(warehouse, '')!=''",
|
||||
"qty > received_qty",
|
||||
],
|
||||
"links": {"item_code": ["Item", "name"], "warehouse": ["Warehouse", "name"]},
|
||||
},
|
||||
"Sales Order Item": {
|
||||
"columns": [
|
||||
"item.name as name",
|
||||
"item_code",
|
||||
"(qty - delivered_qty)*conversion_factor as qty",
|
||||
"warehouse",
|
||||
],
|
||||
"from": "`tabSales Order Item` item, `tabSales Order` main",
|
||||
"conditions": [
|
||||
"item.parent = main.name",
|
||||
"main.docstatus=1",
|
||||
"main.status != 'Stopped'",
|
||||
"ifnull(warehouse, '')!=''",
|
||||
"qty > delivered_qty",
|
||||
],
|
||||
"links": {"item_code": ["Item", "name"], "warehouse": ["Warehouse", "name"]},
|
||||
},
|
||||
# Sales
|
||||
"Customer": {
|
||||
"columns": [
|
||||
"name",
|
||||
"if(customer_name=name, '', customer_name) as customer_name",
|
||||
"customer_group as parent_customer_group",
|
||||
"territory as parent_territory",
|
||||
],
|
||||
"conditions": ["docstatus < 2"],
|
||||
"order_by": "name",
|
||||
"links": {
|
||||
"parent_customer_group": ["Customer Group", "name"],
|
||||
"parent_territory": ["Territory", "name"],
|
||||
},
|
||||
},
|
||||
"Customer Group": {
|
||||
"columns": ["name", "parent_customer_group"],
|
||||
"conditions": ["docstatus < 2"],
|
||||
"order_by": "lft",
|
||||
},
|
||||
"Territory": {
|
||||
"columns": ["name", "parent_territory"],
|
||||
"conditions": ["docstatus < 2"],
|
||||
"order_by": "lft",
|
||||
},
|
||||
"Sales Invoice": {
|
||||
"columns": ["name", "customer", "posting_date", "company"],
|
||||
"conditions": ["docstatus=1"],
|
||||
"order_by": "posting_date",
|
||||
"links": {"customer": ["Customer", "name"], "company": ["Company", "name"]},
|
||||
},
|
||||
"Sales Invoice Item": {
|
||||
"columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"],
|
||||
"conditions": ["docstatus=1", "ifnull(parent, '')!=''"],
|
||||
"order_by": "parent",
|
||||
"links": {"parent": ["Sales Invoice", "name"], "item_code": ["Item", "name"]},
|
||||
},
|
||||
"Sales Order": {
|
||||
"columns": ["name", "customer", "transaction_date as posting_date", "company"],
|
||||
"conditions": ["docstatus=1"],
|
||||
"order_by": "transaction_date",
|
||||
"links": {"customer": ["Customer", "name"], "company": ["Company", "name"]},
|
||||
},
|
||||
"Sales Order Item[Sales Analytics]": {
|
||||
"columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"],
|
||||
"conditions": ["docstatus=1", "ifnull(parent, '')!=''"],
|
||||
"order_by": "parent",
|
||||
"links": {"parent": ["Sales Order", "name"], "item_code": ["Item", "name"]},
|
||||
},
|
||||
"Delivery Note": {
|
||||
"columns": ["name", "customer", "posting_date", "company"],
|
||||
"conditions": ["docstatus=1"],
|
||||
"order_by": "posting_date",
|
||||
"links": {"customer": ["Customer", "name"], "company": ["Company", "name"]},
|
||||
},
|
||||
"Delivery Note Item[Sales Analytics]": {
|
||||
"columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"],
|
||||
"conditions": ["docstatus=1", "ifnull(parent, '')!=''"],
|
||||
"order_by": "parent",
|
||||
"links": {"parent": ["Delivery Note", "name"], "item_code": ["Item", "name"]},
|
||||
},
|
||||
"Supplier": {
|
||||
"columns": [
|
||||
"name",
|
||||
"if(supplier_name=name, '', supplier_name) as supplier_name",
|
||||
"supplier_group as parent_supplier_group",
|
||||
],
|
||||
"conditions": ["docstatus < 2"],
|
||||
"order_by": "name",
|
||||
"links": {
|
||||
"parent_supplier_group": ["Supplier Group", "name"],
|
||||
},
|
||||
},
|
||||
"Supplier Group": {
|
||||
"columns": ["name", "parent_supplier_group"],
|
||||
"conditions": ["docstatus < 2"],
|
||||
"order_by": "name",
|
||||
},
|
||||
"Purchase Invoice": {
|
||||
"columns": ["name", "supplier", "posting_date", "company"],
|
||||
"conditions": ["docstatus=1"],
|
||||
"order_by": "posting_date",
|
||||
"links": {"supplier": ["Supplier", "name"], "company": ["Company", "name"]},
|
||||
},
|
||||
"Purchase Invoice Item": {
|
||||
"columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"],
|
||||
"conditions": ["docstatus=1", "ifnull(parent, '')!=''"],
|
||||
"order_by": "parent",
|
||||
"links": {"parent": ["Purchase Invoice", "name"], "item_code": ["Item", "name"]},
|
||||
},
|
||||
"Purchase Order": {
|
||||
"columns": ["name", "supplier", "transaction_date as posting_date", "company"],
|
||||
"conditions": ["docstatus=1"],
|
||||
"order_by": "posting_date",
|
||||
"links": {"supplier": ["Supplier", "name"], "company": ["Company", "name"]},
|
||||
},
|
||||
"Purchase Order Item[Purchase Analytics]": {
|
||||
"columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"],
|
||||
"conditions": ["docstatus=1", "ifnull(parent, '')!=''"],
|
||||
"order_by": "parent",
|
||||
"links": {"parent": ["Purchase Order", "name"], "item_code": ["Item", "name"]},
|
||||
},
|
||||
"Purchase Receipt": {
|
||||
"columns": ["name", "supplier", "posting_date", "company"],
|
||||
"conditions": ["docstatus=1"],
|
||||
"order_by": "posting_date",
|
||||
"links": {"supplier": ["Supplier", "name"], "company": ["Company", "name"]},
|
||||
},
|
||||
"Purchase Receipt Item[Purchase Analytics]": {
|
||||
"columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"],
|
||||
"conditions": ["docstatus=1", "ifnull(parent, '')!=''"],
|
||||
"order_by": "parent",
|
||||
"links": {"parent": ["Purchase Receipt", "name"], "item_code": ["Item", "name"]},
|
||||
},
|
||||
# Support
|
||||
"Issue": {
|
||||
"columns": ["name", "status", "creation", "resolution_date", "first_responded_on"],
|
||||
"conditions": ["docstatus < 2"],
|
||||
"order_by": "creation",
|
||||
},
|
||||
# Manufacturing
|
||||
"Work Order": {
|
||||
"columns": [
|
||||
"name",
|
||||
"status",
|
||||
"creation",
|
||||
"planned_start_date",
|
||||
"planned_end_date",
|
||||
"status",
|
||||
"actual_start_date",
|
||||
"actual_end_date",
|
||||
"modified",
|
||||
],
|
||||
"conditions": ["docstatus = 1"],
|
||||
"order_by": "creation",
|
||||
},
|
||||
# Medical
|
||||
"Patient": {
|
||||
"columns": [
|
||||
"name",
|
||||
"creation",
|
||||
"owner",
|
||||
"if(patient_name=name, '', patient_name) as patient_name",
|
||||
],
|
||||
"conditions": ["docstatus < 2"],
|
||||
"order_by": "name",
|
||||
"links": {"owner": ["User", "name"]},
|
||||
},
|
||||
"Patient Appointment": {
|
||||
"columns": [
|
||||
"name",
|
||||
"appointment_type",
|
||||
"patient",
|
||||
"practitioner",
|
||||
"appointment_date",
|
||||
"department",
|
||||
"status",
|
||||
"company",
|
||||
],
|
||||
"order_by": "name",
|
||||
"links": {
|
||||
"practitioner": ["Healthcare Practitioner", "name"],
|
||||
"appointment_type": ["Appointment Type", "name"],
|
||||
},
|
||||
},
|
||||
"Healthcare Practitioner": {
|
||||
"columns": ["name", "department"],
|
||||
"order_by": "name",
|
||||
"links": {
|
||||
"department": ["Department", "name"],
|
||||
},
|
||||
},
|
||||
"Appointment Type": {"columns": ["name"], "order_by": "name"},
|
||||
"Medical Department": {"columns": ["name"], "order_by": "name"},
|
||||
}
|
||||
@@ -285,7 +285,7 @@ def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None):
|
||||
batches = get_batches(item_code, warehouse, qty, throw, serial_no)
|
||||
|
||||
for batch in batches:
|
||||
if cint(qty) <= cint(batch.qty):
|
||||
if flt(qty) <= flt(batch.qty):
|
||||
batch_no = batch.batch_id
|
||||
break
|
||||
|
||||
|
||||
@@ -490,7 +490,6 @@
|
||||
"fieldname": "ignore_pricing_rule",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Pricing Rule",
|
||||
"no_copy": 1,
|
||||
"permlevel": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
@@ -1336,7 +1335,7 @@
|
||||
"idx": 146,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-06-10 03:52:04.197415",
|
||||
"modified": "2022-09-16 17:46:17.701904",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note",
|
||||
|
||||
@@ -832,6 +832,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
update_address(
|
||||
target_doc, "shipping_address", "shipping_address_display", source_doc.customer_address
|
||||
)
|
||||
update_address(
|
||||
target_doc, "billing_address", "billing_address_display", source_doc.customer_address
|
||||
)
|
||||
|
||||
update_taxes(
|
||||
target_doc,
|
||||
|
||||
@@ -6,7 +6,7 @@ import json
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import cstr, flt, nowdate, nowtime
|
||||
from frappe.utils import add_days, cstr, flt, nowdate, nowtime, today
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||
from erpnext.accounts.utils import get_balance_on
|
||||
@@ -1050,9 +1050,22 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
dn.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"description": "Tax 1",
|
||||
"rate": 14,
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"included_in_print_rate": 1,
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(dn.items[0].rate, 500) # haven't saved yet
|
||||
dn.save()
|
||||
self.assertEqual(dn.ignore_pricing_rule, 1)
|
||||
self.assertEqual(dn.taxes[0].included_in_print_rate, 0)
|
||||
|
||||
# rate should reset to incoming rate
|
||||
self.assertEqual(dn.items[0].rate, rate)
|
||||
@@ -1063,6 +1076,7 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
dn.save()
|
||||
|
||||
self.assertEqual(dn.items[0].rate, rate)
|
||||
self.assertEqual(dn.items[0].net_rate, rate)
|
||||
|
||||
def test_internal_transfer_precision_gle(self):
|
||||
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
|
||||
@@ -1091,6 +1105,36 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
frappe.db.exists("GL Entry", {"voucher_no": dn.name, "voucher_type": dn.doctype})
|
||||
)
|
||||
|
||||
def test_batch_expiry_for_delivery_note(self):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
|
||||
item = make_item(
|
||||
"_Test Batch Item For Return Check",
|
||||
{
|
||||
"is_purchase_item": 1,
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TBIRC.#####",
|
||||
},
|
||||
)
|
||||
|
||||
pi = make_purchase_receipt(qty=1, item_code=item.name)
|
||||
|
||||
dn = create_delivery_note(qty=1, item_code=item.name, batch_no=pi.items[0].batch_no)
|
||||
|
||||
dn.load_from_db()
|
||||
batch_no = dn.items[0].batch_no
|
||||
self.assertTrue(batch_no)
|
||||
|
||||
frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1))
|
||||
|
||||
return_dn = make_return_doc(dn.doctype, dn.name)
|
||||
return_dn.save().submit()
|
||||
|
||||
self.assertTrue(return_dn.docstatus == 1)
|
||||
|
||||
|
||||
def create_delivery_note(**args):
|
||||
dn = frappe.new_doc("Delivery Note")
|
||||
@@ -1117,6 +1161,7 @@ def create_delivery_note(**args):
|
||||
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
|
||||
"cost_center": args.cost_center or "_Test Cost Center - _TC",
|
||||
"serial_no": args.serial_no,
|
||||
"batch_no": args.batch_no or None,
|
||||
"target_warehouse": args.target_warehouse,
|
||||
},
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user