mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-21 13:44:03 +00:00
Compare commits
189 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53024be347 | ||
|
|
a1f31013e2 | ||
|
|
64d96f436b | ||
|
|
fda250606a | ||
|
|
2126dee8ca | ||
|
|
937d358fbf | ||
|
|
366cd6171c | ||
|
|
a1b0813966 | ||
|
|
4db588e72e | ||
|
|
246869dd28 | ||
|
|
cf087103cb | ||
|
|
fdcc591a5e | ||
|
|
0df96e1084 | ||
|
|
22e7f03a03 | ||
|
|
6aab9ca427 | ||
|
|
57b03f0bf2 | ||
|
|
5a5b49b61a | ||
|
|
37fad7e04c | ||
|
|
7c0069c0d3 | ||
|
|
0b9a59c605 | ||
|
|
b9c326e3f6 | ||
|
|
d9756d54ad | ||
|
|
7ee18e86a2 | ||
|
|
5df50588cf | ||
|
|
87383680f0 | ||
|
|
ee54bf7fe2 | ||
|
|
d5319a4826 | ||
|
|
a98263cd31 | ||
|
|
a3de0320aa | ||
|
|
f7bf4a3e62 | ||
|
|
2e62d518e8 | ||
|
|
731107d0a3 | ||
|
|
645ee2d822 | ||
|
|
b45db347cc | ||
|
|
3cdbb65b5a | ||
|
|
f693d43cda | ||
|
|
b33317e987 | ||
|
|
980a87417b | ||
|
|
93482f3302 | ||
|
|
38fbd94ac9 | ||
|
|
b01f8555e5 | ||
|
|
edbf5513da | ||
|
|
b77b3780d6 | ||
|
|
016839e573 | ||
|
|
25bc10e0a9 | ||
|
|
87ec9cd0cc | ||
|
|
2f42cb6362 | ||
|
|
30d1cecf60 | ||
|
|
4522826335 | ||
|
|
1834671d59 | ||
|
|
dedb90ea72 | ||
|
|
fe9f32946c | ||
|
|
a2d95fc62b | ||
|
|
f70fca1c9e | ||
|
|
30bf48e1db | ||
|
|
153b41a269 | ||
|
|
d99cc8b05a | ||
|
|
12f5d65271 | ||
|
|
57485b30b8 | ||
|
|
1a74c6ee56 | ||
|
|
728ac1f54e | ||
|
|
44f0b69152 | ||
|
|
e69c71576d | ||
|
|
664a2989a6 | ||
|
|
8335ca6331 | ||
|
|
ea0fe5e10c | ||
|
|
6dddbb9f27 | ||
|
|
a074d12afd | ||
|
|
712443a91c | ||
|
|
b585262842 | ||
|
|
a6d0938591 | ||
|
|
198bdcfdc6 | ||
|
|
32cff94bde | ||
|
|
c339305e9c | ||
|
|
ac433dce2a | ||
|
|
d8944b0499 | ||
|
|
f58d3b4d3d | ||
|
|
269e1923c9 | ||
|
|
ba635145da | ||
|
|
dc2f694547 | ||
|
|
b656ffa45e | ||
|
|
0b4e3f1467 | ||
|
|
e76220e819 | ||
|
|
77e8c542dd | ||
|
|
6291b28c37 | ||
|
|
4389100fa1 | ||
|
|
f266d765fd | ||
|
|
5915a73939 | ||
|
|
f6898344ef | ||
|
|
2a29aefb19 | ||
|
|
377c37a99f | ||
|
|
baab3797ca | ||
|
|
8c291f7b7a | ||
|
|
381e9d2236 | ||
|
|
38002e3fe2 | ||
|
|
9506dbe433 | ||
|
|
61ff1c22b3 | ||
|
|
f9d89c7ce6 | ||
|
|
34437a83df | ||
|
|
bed9e09153 | ||
|
|
952e267e92 | ||
|
|
8e30af84cd | ||
|
|
d6126b6695 | ||
|
|
93fe840844 | ||
|
|
183ff53ccb | ||
|
|
fb127da489 | ||
|
|
933bd7413a | ||
|
|
fb9d640d8b | ||
|
|
62bad90c90 | ||
|
|
a167f9e17f | ||
|
|
e135c452af | ||
|
|
9c5f8415bf | ||
|
|
46a00d1da6 | ||
|
|
3a7ac29907 | ||
|
|
3dcef9352e | ||
|
|
5776881f34 | ||
|
|
0b62784f6f | ||
|
|
ef4eb4705e | ||
|
|
9cf790db9d | ||
|
|
2778123106 | ||
|
|
44be67e6dd | ||
|
|
f604101fea | ||
|
|
4008c95ac6 | ||
|
|
512b480f6a | ||
|
|
d5d6b34380 | ||
|
|
2c2d5ccb8f | ||
|
|
147499bc9c | ||
|
|
dc669b540d | ||
|
|
08e2b9110c | ||
|
|
46d773df5d | ||
|
|
4a48a6aae3 | ||
|
|
e650d99cdd | ||
|
|
23ccadbcae | ||
|
|
bfd8ab6d72 | ||
|
|
2ab431ad36 | ||
|
|
21066a48b6 | ||
|
|
9efc0d1043 | ||
|
|
54ea9319a1 | ||
|
|
f54f9771e4 | ||
|
|
e322e7654a | ||
|
|
c674180f1a | ||
|
|
d079d8771b | ||
|
|
4296fc36cc | ||
|
|
f6c9f052d2 | ||
|
|
c9af4e8ce5 | ||
|
|
e91dea62b8 | ||
|
|
cc3c9c7042 | ||
|
|
8e742d6612 | ||
|
|
37dc11e59a | ||
|
|
1b25a7fe76 | ||
|
|
e69849d333 | ||
|
|
e4bb81d830 | ||
|
|
d66979ff52 | ||
|
|
2ccf58d6ad | ||
|
|
9b6c5f2e54 | ||
|
|
8cac70a63c | ||
|
|
8d4d12c3b0 | ||
|
|
341e8dffd3 | ||
|
|
13f9207b41 | ||
|
|
a4ea9fa801 | ||
|
|
c0cf18d2d6 | ||
|
|
d62928ed65 | ||
|
|
4a7ae5c4a7 | ||
|
|
be438c08db | ||
|
|
709fcd5051 | ||
|
|
560c55935f | ||
|
|
30a86951c3 | ||
|
|
4b77a4de28 | ||
|
|
8932d7040f | ||
|
|
07ff2cb1d3 | ||
|
|
8393d113d4 | ||
|
|
c68a38eb5b | ||
|
|
0c163eef23 | ||
|
|
af039be03e | ||
|
|
39f8ee2ea6 | ||
|
|
a9d5000eab | ||
|
|
00acb000dc | ||
|
|
9336a3413f | ||
|
|
c6f946e3e8 | ||
|
|
d720d15e3d | ||
|
|
2e29cf38bb | ||
|
|
f94afc1fff | ||
|
|
f89c1b2c0c | ||
|
|
52aa3561e3 | ||
|
|
14f46a3e26 | ||
|
|
86c5f4db85 | ||
|
|
fc5fd34621 | ||
|
|
378d15d388 | ||
|
|
3a5f5d5cd0 |
7
.github/helper/install.sh
vendored
7
.github/helper/install.sh
vendored
@@ -2,6 +2,13 @@
|
||||
|
||||
set -e
|
||||
|
||||
# Check for merge conflicts before proceeding
|
||||
python -m compileall -f "${GITHUB_WORKSPACE}"
|
||||
if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}"
|
||||
then echo "Found merge conflicts"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd ~ || exit
|
||||
|
||||
sudo apt-get install redis-server libcups2-dev
|
||||
|
||||
31
.github/workflows/release.yml
vendored
Normal file
31
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Generate Semantic Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- version-13
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Entire Repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Setup Node.js v14
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
- name: Setup dependencies
|
||||
run: |
|
||||
npm install @semantic-release/git @semantic-release/exec --no-save
|
||||
- name: Create Release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
GIT_AUTHOR_NAME: "Frappe PR Bot"
|
||||
GIT_AUTHOR_EMAIL: "developers@frappe.io"
|
||||
GIT_COMMITTER_NAME: "Frappe PR Bot"
|
||||
GIT_COMMITTER_EMAIL: "developers@frappe.io"
|
||||
run: npx semantic-release
|
||||
24
.releaserc
Normal file
24
.releaserc
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"branches": ["version-13"],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer", {
|
||||
"preset": "angular",
|
||||
"releaseRules": [
|
||||
{"breaking": true, "release": false}
|
||||
]
|
||||
},
|
||||
"@semantic-release/release-notes-generator",
|
||||
[
|
||||
"@semantic-release/exec", {
|
||||
"prepareCmd": 'sed -ir "s/[0-9]*\.[0-9]*\.[0-9]*/${nextRelease.version}/" erpnext/__init__.py'
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/git", {
|
||||
"assets": ["erpnext/__init__.py"],
|
||||
"message": "chore(release): Bumped to Version ${nextRelease.version}\n\n${nextRelease.notes}"
|
||||
}
|
||||
],
|
||||
"@semantic-release/github"
|
||||
]
|
||||
}
|
||||
@@ -4,7 +4,8 @@ import frappe
|
||||
|
||||
from erpnext.hooks import regional_overrides
|
||||
|
||||
__version__ = '13.25.2'
|
||||
__version__ = "13.28.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
"""Get default company for user"""
|
||||
|
||||
@@ -386,7 +386,6 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
doc,
|
||||
credit_account,
|
||||
debit_account,
|
||||
against,
|
||||
amount,
|
||||
base_amount,
|
||||
end_date,
|
||||
@@ -570,7 +569,6 @@ def book_revenue_via_journal_entry(
|
||||
doc,
|
||||
credit_account,
|
||||
debit_account,
|
||||
against,
|
||||
amount,
|
||||
base_amount,
|
||||
posting_date,
|
||||
@@ -591,6 +589,7 @@ def book_revenue_via_journal_entry(
|
||||
journal_entry.voucher_type = (
|
||||
"Deferred Revenue" if doc.doctype == "Sales Invoice" else "Deferred Expense"
|
||||
)
|
||||
journal_entry.process_deferred_accounting = deferred_process
|
||||
|
||||
debit_entry = {
|
||||
"account": credit_account,
|
||||
|
||||
@@ -99,7 +99,7 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None):
|
||||
if doctype == "Budget":
|
||||
add_dimension_to_budget_doctype(df.copy(), doc)
|
||||
else:
|
||||
create_custom_field(doctype, df)
|
||||
create_custom_field(doctype, df, ignore_validate=True)
|
||||
|
||||
count += 1
|
||||
|
||||
@@ -115,7 +115,7 @@ def add_dimension_to_budget_doctype(df, doc):
|
||||
}
|
||||
)
|
||||
|
||||
create_custom_field("Budget", df)
|
||||
create_custom_field("Budget", df, ignore_validate=True)
|
||||
|
||||
property_setter = frappe.db.exists("Property Setter", "Budget-budget_against-options")
|
||||
|
||||
@@ -205,10 +205,16 @@ def get_doctypes_with_dimensions():
|
||||
return frappe.get_hooks("accounting_dimension_doctypes")
|
||||
|
||||
|
||||
def get_accounting_dimensions(as_list=True):
|
||||
def get_accounting_dimensions(as_list=True, filters=None):
|
||||
|
||||
if not filters:
|
||||
filters = {"disabled": 0}
|
||||
|
||||
if frappe.flags.accounting_dimensions is None:
|
||||
frappe.flags.accounting_dimensions = frappe.get_all(
|
||||
"Accounting Dimension", fields=["label", "fieldname", "disabled", "document_type"]
|
||||
"Accounting Dimension",
|
||||
fields=["label", "fieldname", "disabled", "document_type"],
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
if as_list:
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import flt, fmt_money, getdate, nowdate
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt, fmt_money, getdate
|
||||
|
||||
import erpnext
|
||||
|
||||
form_grid_templates = {"journal_entries": "templates/form_grid/bank_reconciliation_grid.html"}
|
||||
|
||||
@@ -76,6 +79,52 @@ class BankClearance(Document):
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
|
||||
|
||||
loan_disbursements = (
|
||||
frappe.qb.from_(loan_disbursement)
|
||||
.select(
|
||||
ConstantColumn("Loan Disbursement").as_("payment_document"),
|
||||
loan_disbursement.name.as_("payment_entry"),
|
||||
loan_disbursement.disbursed_amount.as_("credit"),
|
||||
ConstantColumn(0).as_("debit"),
|
||||
loan_disbursement.reference_number.as_("cheque_number"),
|
||||
loan_disbursement.reference_date.as_("cheque_date"),
|
||||
loan_disbursement.disbursement_date.as_("posting_date"),
|
||||
loan_disbursement.applicant.as_("against_account"),
|
||||
)
|
||||
.where(loan_disbursement.docstatus == 1)
|
||||
.where(loan_disbursement.disbursement_date >= self.from_date)
|
||||
.where(loan_disbursement.disbursement_date <= self.to_date)
|
||||
.where(loan_disbursement.clearance_date.isnull())
|
||||
.where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
|
||||
.orderby(loan_disbursement.disbursement_date)
|
||||
.orderby(loan_disbursement.name, frappe.qb.desc)
|
||||
).run(as_dict=1)
|
||||
|
||||
loan_repayment = frappe.qb.DocType("Loan Repayment")
|
||||
|
||||
loan_repayments = (
|
||||
frappe.qb.from_(loan_repayment)
|
||||
.select(
|
||||
ConstantColumn("Loan Repayment").as_("payment_document"),
|
||||
loan_repayment.name.as_("payment_entry"),
|
||||
loan_repayment.amount_paid.as_("debit"),
|
||||
ConstantColumn(0).as_("credit"),
|
||||
loan_repayment.reference_number.as_("cheque_number"),
|
||||
loan_repayment.reference_date.as_("cheque_date"),
|
||||
loan_repayment.applicant.as_("against_account"),
|
||||
loan_repayment.posting_date,
|
||||
)
|
||||
.where(loan_repayment.docstatus == 1)
|
||||
.where(loan_repayment.clearance_date.isnull())
|
||||
.where(loan_repayment.posting_date >= self.from_date)
|
||||
.where(loan_repayment.posting_date <= self.to_date)
|
||||
.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
|
||||
.orderby(loan_repayment.posting_date)
|
||||
.orderby(loan_repayment.name, frappe.qb.desc)
|
||||
).run(as_dict=1)
|
||||
|
||||
pos_sales_invoices, pos_purchase_invoices = [], []
|
||||
if self.include_pos_transactions:
|
||||
pos_sales_invoices = frappe.db.sql(
|
||||
@@ -114,20 +163,29 @@ class BankClearance(Document):
|
||||
|
||||
entries = sorted(
|
||||
list(payment_entries)
|
||||
+ list(journal_entries + list(pos_sales_invoices) + list(pos_purchase_invoices)),
|
||||
key=lambda k: k["posting_date"] or getdate(nowdate()),
|
||||
+ list(journal_entries)
|
||||
+ list(pos_sales_invoices)
|
||||
+ list(pos_purchase_invoices)
|
||||
+ list(loan_disbursements)
|
||||
+ list(loan_repayments),
|
||||
key=lambda k: getdate(k["posting_date"]),
|
||||
)
|
||||
|
||||
self.set("payment_entries", [])
|
||||
self.total_amount = 0.0
|
||||
default_currency = erpnext.get_default_currency()
|
||||
|
||||
for d in entries:
|
||||
row = self.append("payment_entries", {})
|
||||
|
||||
amount = flt(d.get("debit", 0)) - flt(d.get("credit", 0))
|
||||
|
||||
if not d.get("account_currency"):
|
||||
d.account_currency = default_currency
|
||||
|
||||
formatted_amount = fmt_money(abs(amount), 2, d.account_currency)
|
||||
d.amount = formatted_amount + " " + (_("Dr") if amount > 0 else _("Cr"))
|
||||
d.posting_date = getdate(d.posting_date)
|
||||
|
||||
d.pop("credit")
|
||||
d.pop("debit")
|
||||
|
||||
@@ -1,9 +1,96 @@
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_months, getdate
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.loan_management.doctype.loan.test_loan import (
|
||||
create_loan,
|
||||
create_loan_accounts,
|
||||
create_loan_type,
|
||||
create_repayment_entry,
|
||||
make_loan_disbursement_entry,
|
||||
)
|
||||
|
||||
|
||||
class TestBankClearance(unittest.TestCase):
|
||||
pass
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
make_bank_account()
|
||||
create_loan_accounts()
|
||||
create_loan_masters()
|
||||
add_transactions()
|
||||
|
||||
# Basic test case to test if bank clearance tool doesn't break
|
||||
# Detailed test can be added later
|
||||
def test_bank_clearance(self):
|
||||
bank_clearance = frappe.get_doc("Bank Clearance")
|
||||
bank_clearance.account = "_Test Bank Clearance - _TC"
|
||||
bank_clearance.from_date = add_months(getdate(), -1)
|
||||
bank_clearance.to_date = getdate()
|
||||
bank_clearance.get_payment_entries()
|
||||
self.assertEqual(len(bank_clearance.payment_entries), 3)
|
||||
|
||||
|
||||
def make_bank_account():
|
||||
if not frappe.db.get_value("Account", "_Test Bank Clearance - _TC"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_type": "Bank",
|
||||
"account_name": "_Test Bank Clearance",
|
||||
"company": "_Test Company",
|
||||
"parent_account": "Bank Accounts - _TC",
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
||||
def create_loan_masters():
|
||||
create_loan_type(
|
||||
"Clearance Loan",
|
||||
2000000,
|
||||
13.5,
|
||||
25,
|
||||
0,
|
||||
5,
|
||||
"Cash",
|
||||
"_Test Bank Clearance - _TC",
|
||||
"_Test Bank Clearance - _TC",
|
||||
"Loan Account - _TC",
|
||||
"Interest Income Account - _TC",
|
||||
"Penalty Income Account - _TC",
|
||||
)
|
||||
|
||||
|
||||
def add_transactions():
|
||||
make_payment_entry()
|
||||
make_loan()
|
||||
|
||||
|
||||
def make_loan():
|
||||
loan = create_loan(
|
||||
"_Test Customer",
|
||||
"Clearance Loan",
|
||||
280000,
|
||||
"Repay Over Number of Periods",
|
||||
20,
|
||||
applicant_type="Customer",
|
||||
)
|
||||
loan.submit()
|
||||
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=getdate())
|
||||
repayment_entry = create_repayment_entry(loan.name, "_Test Customer", getdate(), loan.loan_amount)
|
||||
repayment_entry.save()
|
||||
repayment_entry.submit()
|
||||
|
||||
|
||||
def make_payment_entry():
|
||||
pi = make_purchase_invoice(supplier="_Test Supplier", qty=1, rate=690)
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank Clearance - _TC")
|
||||
pe.reference_no = "Conrad Oct 18"
|
||||
pe.reference_date = "2018-10-24"
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"sgst_account",
|
||||
"igst_account",
|
||||
"cess_account",
|
||||
"utgst_account",
|
||||
"is_reverse_charge_account"
|
||||
],
|
||||
"fields": [
|
||||
@@ -64,12 +65,18 @@
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Is Reverse Charge Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "utgst_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "UTGST Account",
|
||||
"options": "Account"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-09 12:30:25.889993",
|
||||
"modified": "2022-04-07 12:59:14.039768",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "GST Account",
|
||||
@@ -78,5 +85,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"allow_auto_repeat": 1,
|
||||
"allow_import": 1,
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2013-03-25 10:53:52",
|
||||
"creation": "2022-01-25 10:29:58.717206",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"engine": "InnoDB",
|
||||
@@ -13,6 +13,7 @@
|
||||
"voucher_type",
|
||||
"naming_series",
|
||||
"finance_book",
|
||||
"process_deferred_accounting",
|
||||
"reversal_of",
|
||||
"tax_withholding_category",
|
||||
"column_break1",
|
||||
@@ -524,13 +525,20 @@
|
||||
"label": "Reversal Of",
|
||||
"options": "Journal Entry",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "process_deferred_accounting",
|
||||
"fieldtype": "Link",
|
||||
"label": "Process Deferred Accounting",
|
||||
"options": "Process Deferred Accounting",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 176,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-01-04 13:39:36.485954",
|
||||
"modified": "2022-04-06 17:18:46.865259",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
@@ -578,6 +586,7 @@
|
||||
"search_fields": "voucher_type,posting_date, due_date, cheque_no",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -19,7 +19,6 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
|
||||
)
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.utils import (
|
||||
check_if_stock_and_account_balance_synced,
|
||||
get_account_currency,
|
||||
get_balance_on,
|
||||
get_stock_accounts,
|
||||
@@ -88,9 +87,6 @@ class JournalEntry(AccountsController):
|
||||
self.update_expense_claim()
|
||||
self.update_inter_company_jv()
|
||||
self.update_invoice_discounting()
|
||||
check_if_stock_and_account_balance_synced(
|
||||
self.posting_date, self.company, self.doctype, self.name
|
||||
)
|
||||
|
||||
def on_cancel(self):
|
||||
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
|
||||
|
||||
@@ -114,10 +114,13 @@ class OpeningInvoiceCreationTool(Document):
|
||||
)
|
||||
or {}
|
||||
)
|
||||
|
||||
default_currency = frappe.db.get_value(row.party_type, row.party, "default_currency")
|
||||
|
||||
if company_details:
|
||||
invoice.update(
|
||||
{
|
||||
"currency": company_details.get("default_currency"),
|
||||
"currency": default_currency or company_details.get("default_currency"),
|
||||
"letter_head": company_details.get("default_letter_head"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -226,10 +226,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
(frm.doc.total_allocated_amount > party_amount)));
|
||||
|
||||
frm.toggle_display("set_exchange_gain_loss",
|
||||
(frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount &&
|
||||
((frm.doc.paid_from_account_currency != company_currency ||
|
||||
frm.doc.paid_to_account_currency != company_currency) &&
|
||||
frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency)));
|
||||
frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount);
|
||||
|
||||
frm.refresh_fields();
|
||||
},
|
||||
|
||||
@@ -38,6 +38,15 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
this.frm.set_query("cost_center", () => {
|
||||
return {
|
||||
"filters": {
|
||||
"company": this.frm.doc.company,
|
||||
"is_group": 0
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function() {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"invoice_limit",
|
||||
"payment_limit",
|
||||
"bank_cash_account",
|
||||
"cost_center",
|
||||
"sec_break1",
|
||||
"invoices",
|
||||
"column_break_15",
|
||||
@@ -178,13 +179,19 @@
|
||||
{
|
||||
"fieldname": "column_break_11",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"icon": "icon-resize-horizontal",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-04 20:27:11.114194",
|
||||
"modified": "2022-04-29 15:37:10.246831",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation",
|
||||
@@ -209,5 +216,6 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -332,6 +332,9 @@ 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):
|
||||
condition = " and cost_center = '{0}' ".format(self.cost_center)
|
||||
|
||||
if get_invoices:
|
||||
condition += (
|
||||
" and posting_date >= {0}".format(frappe.db.escape(self.from_invoice_date))
|
||||
@@ -350,9 +353,13 @@ class PaymentReconciliation(Document):
|
||||
)
|
||||
|
||||
if self.minimum_invoice_amount:
|
||||
condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_invoice_amount))
|
||||
condition += " and {dr_or_cr} >= {amount}".format(
|
||||
dr_or_cr=dr_or_cr, amount=flt(self.minimum_invoice_amount)
|
||||
)
|
||||
if self.maximum_invoice_amount:
|
||||
condition += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_invoice_amount))
|
||||
condition += " and {dr_or_cr} <= {amount}".format(
|
||||
dr_or_cr=dr_or_cr, amount=flt(self.maximum_invoice_amount)
|
||||
)
|
||||
|
||||
elif get_return_invoices:
|
||||
condition = " and doc.company = '{0}' ".format(self.company)
|
||||
@@ -367,15 +374,19 @@ class PaymentReconciliation(Document):
|
||||
else ""
|
||||
)
|
||||
dr_or_cr = (
|
||||
"gl.debit_in_account_currency"
|
||||
"debit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
else "gl.credit_in_account_currency"
|
||||
else "credit_in_account_currency"
|
||||
)
|
||||
|
||||
if self.minimum_invoice_amount:
|
||||
condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_payment_amount))
|
||||
condition += " and gl.{dr_or_cr} >= {amount}".format(
|
||||
dr_or_cr=dr_or_cr, amount=flt(self.minimum_payment_amount)
|
||||
)
|
||||
if self.maximum_invoice_amount:
|
||||
condition += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_payment_amount))
|
||||
condition += " and gl.{dr_or_cr} <= {amount}".format(
|
||||
dr_or_cr=dr_or_cr, amount=flt(self.maximum_payment_amount)
|
||||
)
|
||||
|
||||
else:
|
||||
condition += (
|
||||
|
||||
@@ -1,9 +1,96 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, getdate
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
|
||||
class TestPaymentReconciliation(unittest.TestCase):
|
||||
pass
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
make_customer()
|
||||
make_invoice_and_payment()
|
||||
|
||||
def test_payment_reconciliation(self):
|
||||
payment_reco = frappe.get_doc("Payment Reconciliation")
|
||||
payment_reco.company = "_Test Company"
|
||||
payment_reco.party_type = "Customer"
|
||||
payment_reco.party = "_Test Payment Reco Customer"
|
||||
payment_reco.receivable_payable_account = "Debtors - _TC"
|
||||
payment_reco.from_invoice_date = add_days(getdate(), -1)
|
||||
payment_reco.to_invoice_date = getdate()
|
||||
payment_reco.from_payment_date = add_days(getdate(), -1)
|
||||
payment_reco.to_payment_date = getdate()
|
||||
payment_reco.maximum_invoice_amount = 1000
|
||||
payment_reco.maximum_payment_amount = 1000
|
||||
payment_reco.invoice_limit = 10
|
||||
payment_reco.payment_limit = 10
|
||||
payment_reco.bank_cash_account = "_Test Bank - _TC"
|
||||
payment_reco.cost_center = "_Test Cost Center - _TC"
|
||||
payment_reco.get_unreconciled_entries()
|
||||
|
||||
self.assertEqual(len(payment_reco.get("invoices")), 1)
|
||||
self.assertEqual(len(payment_reco.get("payments")), 1)
|
||||
|
||||
payment_entry = payment_reco.get("payments")[0].reference_name
|
||||
invoice = payment_reco.get("invoices")[0].invoice_number
|
||||
|
||||
payment_reco.allocate_entries(
|
||||
{
|
||||
"payments": [payment_reco.get("payments")[0].as_dict()],
|
||||
"invoices": [payment_reco.get("invoices")[0].as_dict()],
|
||||
}
|
||||
)
|
||||
payment_reco.reconcile()
|
||||
|
||||
payment_entry_doc = frappe.get_doc("Payment Entry", payment_entry)
|
||||
self.assertEqual(payment_entry_doc.get("references")[0].reference_name, invoice)
|
||||
|
||||
|
||||
def make_customer():
|
||||
if not frappe.db.get_value("Customer", "_Test Payment Reco Customer"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Customer",
|
||||
"customer_name": "_Test Payment Reco Customer",
|
||||
"customer_type": "Individual",
|
||||
"customer_group": "_Test Customer Group",
|
||||
"territory": "_Test Territory",
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
||||
def make_invoice_and_payment():
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Payment Reco Customer", qty=1, rate=690, do_not_save=True
|
||||
)
|
||||
si.cost_center = "_Test Cost Center - _TC"
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
pe = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Entry",
|
||||
"payment_type": "Receive",
|
||||
"party_type": "Customer",
|
||||
"party": "_Test Payment Reco Customer",
|
||||
"company": "_Test Company",
|
||||
"paid_from_account_currency": "INR",
|
||||
"paid_to_account_currency": "INR",
|
||||
"source_exchange_rate": 1,
|
||||
"target_exchange_rate": 1,
|
||||
"reference_no": "1",
|
||||
"reference_date": getdate(),
|
||||
"received_amount": 690,
|
||||
"paid_amount": 690,
|
||||
"paid_from": "Debtors - _TC",
|
||||
"paid_to": "_Test Bank - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
}
|
||||
)
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
@@ -166,7 +166,7 @@ class PeriodClosingVoucher(AccountsController):
|
||||
sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency,
|
||||
sum(t1.debit) - sum(t1.credit) as bal_in_company_currency
|
||||
from `tabGL Entry` t1, `tabAccount` t2
|
||||
where t1.account = t2.name and t2.report_type = 'Profit and Loss'
|
||||
where t1.is_cancelled = 0 and t1.account = t2.name and t2.report_type = 'Profit and Loss'
|
||||
and t2.docstatus < 2 and t2.company = %s
|
||||
and t1.posting_date between %s and %s
|
||||
group by t1.account, {dimension_fields}
|
||||
|
||||
@@ -11,7 +11,7 @@ from erpnext.accounts.deferred_revenue import (
|
||||
convert_deferred_expense_to_expense,
|
||||
convert_deferred_revenue_to_income,
|
||||
)
|
||||
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
|
||||
|
||||
class ProcessDeferredAccounting(Document):
|
||||
@@ -34,4 +34,4 @@ class ProcessDeferredAccounting(Document):
|
||||
filters={"against_voucher_type": self.doctype, "against_voucher": self.name},
|
||||
)
|
||||
|
||||
make_reverse_gl_entries(gl_entries=gl_entries)
|
||||
make_gl_entries(gl_entries=gl_entries, cancel=1)
|
||||
|
||||
@@ -34,8 +34,9 @@ class ProcessStatementOfAccounts(Document):
|
||||
frappe.throw(_("Customers not selected."))
|
||||
|
||||
if self.enable_auto_email:
|
||||
self.to_date = self.start_date
|
||||
self.from_date = add_months(self.to_date, -1 * self.filter_duration)
|
||||
if self.start_date and getdate(self.start_date) >= getdate(today()):
|
||||
self.to_date = self.start_date
|
||||
self.from_date = add_months(self.to_date, -1 * self.filter_duration)
|
||||
|
||||
|
||||
def get_report_pdf(doc, consolidated=True):
|
||||
|
||||
@@ -30,6 +30,9 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
|
||||
onload: function() {
|
||||
this._super();
|
||||
|
||||
// Ignore linked advances
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry'];
|
||||
|
||||
if(!this.frm.doc.__islocal) {
|
||||
// show credit_to in print format
|
||||
if(!this.frm.doc.supplier && this.frm.doc.credit_to) {
|
||||
|
||||
@@ -801,7 +801,9 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
if provisional_accounting_for_non_stock_items:
|
||||
if item.purchase_receipt:
|
||||
provisional_account = self.get_company_default("default_provisional_account")
|
||||
provisional_account = frappe.db.get_value(
|
||||
"Purchase Receipt Item", item.pr_detail, "provisional_expense_account"
|
||||
) or self.get_company_default("default_provisional_account")
|
||||
purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt)
|
||||
|
||||
if not purchase_receipt_doc:
|
||||
@@ -824,7 +826,7 @@ class PurchaseInvoice(BuyingController):
|
||||
if expense_booked_in_pr:
|
||||
# Intentionally passing purchase invoice item to handle partial billing
|
||||
purchase_receipt_doc.add_provisional_gl_entry(
|
||||
item, gl_entries, self.posting_date, reverse=1
|
||||
item, gl_entries, self.posting_date, provisional_account, reverse=1
|
||||
)
|
||||
|
||||
if not self.is_internal_transfer():
|
||||
@@ -1315,7 +1317,9 @@ class PurchaseInvoice(BuyingController):
|
||||
if (
|
||||
not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment
|
||||
):
|
||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(self.company)
|
||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||
self.company, "Purchase Invoice", self.name
|
||||
)
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
|
||||
@@ -1449,7 +1449,8 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
|
||||
|
||||
def test_provisional_accounting_entry(self):
|
||||
item = create_item("_Test Non Stock Item", is_stock_item=0)
|
||||
create_item("_Test Non Stock Item", is_stock_item=0)
|
||||
|
||||
provisional_account = create_account(
|
||||
account_name="Provision Account",
|
||||
parent_account="Current Liabilities - _TC",
|
||||
@@ -1472,6 +1473,8 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
self.assertEquals(pr.items[0].provisional_expense_account, "Provision Account - _TC")
|
||||
|
||||
# Check GLE for Purchase Invoice
|
||||
expected_gle = [
|
||||
["Cost of Goods Sold - _TC", 250, 0, add_days(pr.posting_date, -1)],
|
||||
|
||||
@@ -34,7 +34,9 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
|
||||
var me = this;
|
||||
this._super();
|
||||
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', 'POS Closing Entry'];
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
|
||||
'POS Closing Entry', 'Journal Entry', 'Payment Entry'];
|
||||
|
||||
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
|
||||
// show debit_to in print format
|
||||
this.frm.set_df_property("debit_to", "print_hide", 0);
|
||||
|
||||
@@ -1473,7 +1473,9 @@ class SalesInvoice(SellingController):
|
||||
and self.base_rounding_adjustment
|
||||
and not self.is_internal_transfer()
|
||||
):
|
||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(self.company)
|
||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||
self.company, "Sales Invoice", self.name
|
||||
)
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
|
||||
@@ -2007,6 +2007,13 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
self.assertEqual(expected_values[gle.account][2], gle.credit)
|
||||
|
||||
def test_rounding_adjustment_3(self):
|
||||
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
|
||||
create_dimension,
|
||||
disable_dimension,
|
||||
)
|
||||
|
||||
create_dimension()
|
||||
|
||||
si = create_sales_invoice(do_not_save=True)
|
||||
si.items = []
|
||||
for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]:
|
||||
@@ -2034,6 +2041,10 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
"included_in_print_rate": 1,
|
||||
},
|
||||
)
|
||||
|
||||
si.cost_center = "_Test Cost Center 2 - _TC"
|
||||
si.location = "Block 1"
|
||||
|
||||
si.save()
|
||||
si.submit()
|
||||
self.assertEqual(si.net_total, 4007.16)
|
||||
@@ -2069,6 +2080,18 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
self.assertEqual(debit_credit_diff, 0)
|
||||
|
||||
round_off_gle = frappe.db.get_value(
|
||||
"GL Entry",
|
||||
{"voucher_type": "Sales Invoice", "voucher_no": si.name, "account": "Round Off - _TC"},
|
||||
["cost_center", "location"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
self.assertEqual(round_off_gle.cost_center, "_Test Cost Center 2 - _TC")
|
||||
self.assertEqual(round_off_gle.location, "Block 1")
|
||||
|
||||
disable_dimension()
|
||||
|
||||
def test_sales_invoice_with_shipping_rule(self):
|
||||
from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule
|
||||
|
||||
@@ -2270,6 +2293,14 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, "2019-01-30")
|
||||
|
||||
def test_deferred_revenue_missing_account(self):
|
||||
si = create_sales_invoice(posting_date="2019-01-10", do_not_submit=True)
|
||||
si.items[0].enable_deferred_revenue = 1
|
||||
si.items[0].service_start_date = "2019-01-10"
|
||||
si.items[0].service_end_date = "2019-03-15"
|
||||
|
||||
self.assertRaises(frappe.ValidationError, si.save)
|
||||
|
||||
def test_fixed_deferred_revenue(self):
|
||||
deferred_account = create_account(
|
||||
account_name="Deferred Revenue",
|
||||
@@ -3042,7 +3073,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
acc_settings = frappe.get_single("Accounts Settings")
|
||||
acc_settings.book_deferred_entries_via_journal_entry = 0
|
||||
acc_settings.submit_journal_entriessubmit_journal_entries = 0
|
||||
acc_settings.submit_journal_entries = 0
|
||||
acc_settings.save()
|
||||
|
||||
frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None)
|
||||
@@ -3054,6 +3085,62 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
si.reload()
|
||||
self.assertTrue(si.items[0].serial_no)
|
||||
|
||||
def test_gain_loss_with_advance_entry(self):
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
|
||||
unlink_enabled = frappe.db.get_value(
|
||||
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice"
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1
|
||||
)
|
||||
|
||||
jv = make_journal_entry("_Test Receivable USD - _TC", "_Test Bank - _TC", -7000, save=False)
|
||||
|
||||
jv.accounts[0].exchange_rate = 70
|
||||
jv.accounts[0].credit_in_account_currency = 100
|
||||
jv.accounts[0].party_type = "Customer"
|
||||
jv.accounts[0].party = "_Test Customer USD"
|
||||
|
||||
jv.save()
|
||||
jv.submit()
|
||||
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
debit_to="_Test Receivable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=75,
|
||||
do_not_save=1,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
si.append(
|
||||
"advances",
|
||||
{
|
||||
"reference_type": "Journal Entry",
|
||||
"reference_name": jv.name,
|
||||
"reference_row": jv.accounts[0].name,
|
||||
"advance_amount": 100,
|
||||
"allocated_amount": 100,
|
||||
"ref_exchange_rate": 70,
|
||||
},
|
||||
)
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
expected_gle = [
|
||||
["_Test Receivable USD - _TC", 7500.0, 500],
|
||||
["Exchange Gain/Loss - _TC", 500.0, 0.0],
|
||||
["Sales - _TC", 0.0, 7500.0],
|
||||
]
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, nowdate())
|
||||
|
||||
frappe.db.set_value(
|
||||
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled
|
||||
)
|
||||
|
||||
|
||||
def get_sales_invoice_for_e_invoice():
|
||||
si = make_sales_invoice_for_ewaybill()
|
||||
|
||||
@@ -273,7 +273,7 @@ def round_off_debit_credit(gl_map):
|
||||
|
||||
def make_round_off_gle(gl_map, debit_credit_diff, precision):
|
||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||
gl_map[0].company
|
||||
gl_map[0].company, gl_map[0].voucher_type, gl_map[0].voucher_no
|
||||
)
|
||||
round_off_account_exists = False
|
||||
round_off_gle = frappe._dict()
|
||||
@@ -310,14 +310,43 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
|
||||
}
|
||||
)
|
||||
|
||||
update_accounting_dimensions(round_off_gle)
|
||||
|
||||
if not round_off_account_exists:
|
||||
gl_map.append(round_off_gle)
|
||||
|
||||
|
||||
def get_round_off_account_and_cost_center(company):
|
||||
def update_accounting_dimensions(round_off_gle):
|
||||
dimensions = get_accounting_dimensions()
|
||||
meta = frappe.get_meta(round_off_gle["voucher_type"])
|
||||
has_all_dimensions = True
|
||||
|
||||
for dimension in dimensions:
|
||||
if not meta.has_field(dimension):
|
||||
has_all_dimensions = False
|
||||
|
||||
if dimensions and has_all_dimensions:
|
||||
dimension_values = frappe.db.get_value(
|
||||
round_off_gle["voucher_type"], round_off_gle["voucher_no"], dimensions, as_dict=1
|
||||
)
|
||||
|
||||
for dimension in dimensions:
|
||||
round_off_gle[dimension] = dimension_values.get(dimension)
|
||||
|
||||
|
||||
def get_round_off_account_and_cost_center(company, voucher_type, voucher_no):
|
||||
round_off_account, round_off_cost_center = frappe.get_cached_value(
|
||||
"Company", company, ["round_off_account", "round_off_cost_center"]
|
||||
) or [None, None]
|
||||
|
||||
meta = frappe.get_meta(voucher_type)
|
||||
|
||||
# Give first preference to parent cost center for round off GLE
|
||||
if meta.has_field("cost_center"):
|
||||
parent_cost_center = frappe.db.get_value(voucher_type, voucher_no, "cost_center")
|
||||
if parent_cost_center:
|
||||
round_off_cost_center = parent_cost_center
|
||||
|
||||
if not round_off_account:
|
||||
frappe.throw(_("Please mention Round Off Account in Company"))
|
||||
|
||||
|
||||
@@ -53,6 +53,22 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname": "party_account",
|
||||
"label": __("Payable Account"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Account",
|
||||
get_query: () => {
|
||||
var company = frappe.query_report.get_filter_value('company');
|
||||
return {
|
||||
filters: {
|
||||
'company': company,
|
||||
'account_type': 'Payable',
|
||||
'is_group': 0
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname": "ageing_based_on",
|
||||
"label": __("Ageing Based On"),
|
||||
|
||||
@@ -66,6 +66,22 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname": "party_account",
|
||||
"label": __("Receivable Account"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Account",
|
||||
get_query: () => {
|
||||
var company = frappe.query_report.get_filter_value('company');
|
||||
return {
|
||||
filters: {
|
||||
'company': company,
|
||||
'account_type': 'Receivable',
|
||||
'is_group': 0
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname": "ageing_based_on",
|
||||
"label": __("Ageing Based On"),
|
||||
|
||||
@@ -111,6 +111,7 @@ class ReceivablePayableReport(object):
|
||||
voucher_type=gle.voucher_type,
|
||||
voucher_no=gle.voucher_no,
|
||||
party=gle.party,
|
||||
party_account=gle.account,
|
||||
posting_date=gle.posting_date,
|
||||
account_currency=gle.account_currency,
|
||||
remarks=gle.remarks if self.filters.get("show_remarks") else None,
|
||||
@@ -777,18 +778,21 @@ class ReceivablePayableReport(object):
|
||||
conditions.append("party=%s")
|
||||
values.append(self.filters.get(party_type_field))
|
||||
|
||||
# get GL with "receivable" or "payable" account_type
|
||||
account_type = "Receivable" if self.party_type == "Customer" else "Payable"
|
||||
accounts = [
|
||||
d.name
|
||||
for d in frappe.get_all(
|
||||
"Account", filters={"account_type": account_type, "company": self.filters.company}
|
||||
)
|
||||
]
|
||||
|
||||
if accounts:
|
||||
conditions.append("account in (%s)" % ",".join(["%s"] * len(accounts)))
|
||||
values += accounts
|
||||
if self.filters.party_account:
|
||||
conditions.append("account =%s")
|
||||
values.append(self.filters.party_account)
|
||||
else:
|
||||
# get GL with "receivable" or "payable" account_type
|
||||
account_type = "Receivable" if self.party_type == "Customer" else "Payable"
|
||||
accounts = [
|
||||
d.name
|
||||
for d in frappe.get_all(
|
||||
"Account", filters={"account_type": account_type, "company": self.filters.company}
|
||||
)
|
||||
]
|
||||
if accounts:
|
||||
conditions.append("account in (%s)" % ",".join(["%s"] * len(accounts)))
|
||||
values += accounts
|
||||
|
||||
def add_customer_filters(self, conditions, values):
|
||||
if self.filters.get("customer_group"):
|
||||
@@ -889,6 +893,14 @@ class ReceivablePayableReport(object):
|
||||
width=180,
|
||||
)
|
||||
|
||||
self.add_column(
|
||||
label="Receivable Account" if self.party_type == "Customer" else "Payable Account",
|
||||
fieldname="party_account",
|
||||
fieldtype="Link",
|
||||
options="Account",
|
||||
width=180,
|
||||
)
|
||||
|
||||
if self.party_naming_by == "Naming Series":
|
||||
self.add_column(
|
||||
_("{0} Name").format(self.party_type),
|
||||
|
||||
@@ -50,12 +50,19 @@ class TestAccountsReceivable(unittest.TestCase):
|
||||
make_credit_note(name)
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_credit_note = [100, 0, 0, 40, -40]
|
||||
expected_data_after_credit_note = [100, 0, 0, 40, -40, "Debtors - _TC2"]
|
||||
|
||||
row = report[1][0]
|
||||
self.assertEqual(
|
||||
expected_data_after_credit_note,
|
||||
[row.invoice_grand_total, row.invoiced, row.paid, row.credit_note, row.outstanding],
|
||||
[
|
||||
row.invoice_grand_total,
|
||||
row.invoiced,
|
||||
row.paid,
|
||||
row.credit_note,
|
||||
row.outstanding,
|
||||
row.party_account,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -124,11 +124,10 @@ def get_columns(invoice_list, additional_table_columns):
|
||||
_("Purchase Receipt") + ":Link/Purchase Receipt:100",
|
||||
{"fieldname": "currency", "label": _("Currency"), "fieldtype": "Data", "width": 80},
|
||||
]
|
||||
expense_accounts = (
|
||||
tax_accounts
|
||||
) = (
|
||||
expense_columns
|
||||
) = tax_columns = unrealized_profit_loss_accounts = unrealized_profit_loss_account_columns = []
|
||||
|
||||
expense_accounts = []
|
||||
tax_accounts = []
|
||||
unrealized_profit_loss_accounts = []
|
||||
|
||||
if invoice_list:
|
||||
expense_accounts = frappe.db.sql_list(
|
||||
@@ -163,10 +162,11 @@ def get_columns(invoice_list, additional_table_columns):
|
||||
unrealized_profit_loss_account_columns = [
|
||||
(account + ":Currency/currency:120") for account in unrealized_profit_loss_accounts
|
||||
]
|
||||
|
||||
for account in tax_accounts:
|
||||
if account not in expense_accounts:
|
||||
tax_columns.append(account + ":Currency/currency:120")
|
||||
tax_columns = [
|
||||
(account + ":Currency/currency:120")
|
||||
for account in tax_accounts
|
||||
if account not in expense_accounts
|
||||
]
|
||||
|
||||
columns = (
|
||||
columns
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.test_runner import make_test_objects
|
||||
|
||||
from erpnext.accounts.party import get_party_shipping_address
|
||||
from erpnext.accounts.utils import get_future_stock_vouchers, get_voucherwise_gl_entries
|
||||
from erpnext.accounts.utils import (
|
||||
get_future_stock_vouchers,
|
||||
get_voucherwise_gl_entries,
|
||||
sort_stock_vouchers_by_posting_date,
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
|
||||
class TestUtils(unittest.TestCase):
|
||||
@@ -47,6 +54,25 @@ class TestUtils(unittest.TestCase):
|
||||
msg="get_voucherwise_gl_entries not returning expected GLes",
|
||||
)
|
||||
|
||||
def test_stock_voucher_sorting(self):
|
||||
vouchers = []
|
||||
|
||||
item = make_item().name
|
||||
|
||||
stock_entry = {"item": item, "to_warehouse": "_Test Warehouse - _TC", "qty": 1, "rate": 10}
|
||||
|
||||
se1 = make_stock_entry(posting_date="2022-01-01", **stock_entry)
|
||||
se2 = make_stock_entry(posting_date="2022-02-01", **stock_entry)
|
||||
se3 = make_stock_entry(posting_date="2022-03-01", **stock_entry)
|
||||
|
||||
for doc in (se1, se2, se3):
|
||||
vouchers.append((doc.doctype, doc.name))
|
||||
|
||||
vouchers.append(("Stock Entry", "Wat"))
|
||||
|
||||
sorted_vouchers = sort_stock_vouchers_by_posting_date(list(reversed(vouchers)))
|
||||
self.assertEqual(sorted_vouchers, vouchers)
|
||||
|
||||
|
||||
ADDRESS_RECORDS = [
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
from json import loads
|
||||
from typing import List, Tuple
|
||||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
@@ -19,10 +20,6 @@ from erpnext.stock import get_warehouse_account_map
|
||||
from erpnext.stock.utils import get_stock_value_on
|
||||
|
||||
|
||||
class StockValueAndAccountBalanceOutOfSync(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class FiscalYearError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
@@ -1127,6 +1124,9 @@ def update_gl_entries_after(
|
||||
def repost_gle_for_stock_vouchers(
|
||||
stock_vouchers, posting_date, company=None, warehouse_account=None
|
||||
):
|
||||
if not stock_vouchers:
|
||||
return
|
||||
|
||||
def _delete_gl_entries(voucher_type, voucher_no):
|
||||
frappe.db.sql(
|
||||
"""delete from `tabGL Entry`
|
||||
@@ -1134,6 +1134,8 @@ def repost_gle_for_stock_vouchers(
|
||||
(voucher_type, voucher_no),
|
||||
)
|
||||
|
||||
stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers)
|
||||
|
||||
if not warehouse_account:
|
||||
warehouse_account = get_warehouse_account_map(company)
|
||||
|
||||
@@ -1154,6 +1156,27 @@ def repost_gle_for_stock_vouchers(
|
||||
_delete_gl_entries(voucher_type, voucher_no)
|
||||
|
||||
|
||||
def sort_stock_vouchers_by_posting_date(
|
||||
stock_vouchers: List[Tuple[str, str]]
|
||||
) -> List[Tuple[str, str]]:
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
voucher_nos = [v[1] for v in stock_vouchers]
|
||||
|
||||
sles = (
|
||||
frappe.qb.from_(sle)
|
||||
.select(sle.voucher_type, sle.voucher_no, sle.posting_date, sle.posting_time, sle.creation)
|
||||
.where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos)))
|
||||
.groupby(sle.voucher_type, sle.voucher_no)
|
||||
).run(as_dict=True)
|
||||
sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles]
|
||||
|
||||
unknown_vouchers = set(stock_vouchers) - set(sorted_vouchers)
|
||||
if unknown_vouchers:
|
||||
sorted_vouchers.extend(unknown_vouchers)
|
||||
|
||||
return sorted_vouchers
|
||||
|
||||
|
||||
def get_future_stock_vouchers(
|
||||
posting_date, posting_time, for_warehouses=None, for_items=None, company=None
|
||||
):
|
||||
@@ -1247,47 +1270,6 @@ def compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
|
||||
return matched
|
||||
|
||||
|
||||
def check_if_stock_and_account_balance_synced(
|
||||
posting_date, company, voucher_type=None, voucher_no=None
|
||||
):
|
||||
if not cint(erpnext.is_perpetual_inventory_enabled(company)):
|
||||
return
|
||||
|
||||
accounts = get_stock_accounts(company, voucher_type, voucher_no)
|
||||
stock_adjustment_account = frappe.db.get_value("Company", company, "stock_adjustment_account")
|
||||
|
||||
for account in accounts:
|
||||
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(
|
||||
account, posting_date, company
|
||||
)
|
||||
|
||||
if abs(account_bal - stock_bal) > 0.1:
|
||||
precision = get_field_precision(
|
||||
frappe.get_meta("GL Entry").get_field("debit"),
|
||||
currency=frappe.get_cached_value("Company", company, "default_currency"),
|
||||
)
|
||||
|
||||
diff = flt(stock_bal - account_bal, precision)
|
||||
|
||||
error_reason = _(
|
||||
"Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses as on {3}."
|
||||
).format(stock_bal, account_bal, frappe.bold(account), posting_date)
|
||||
error_resolution = _("Please create an adjustment Journal Entry for amount {0} on {1}").format(
|
||||
frappe.bold(diff), frappe.bold(posting_date)
|
||||
)
|
||||
|
||||
frappe.msgprint(
|
||||
msg="""{0}<br></br>{1}<br></br>""".format(error_reason, error_resolution),
|
||||
raise_exception=StockValueAndAccountBalanceOutOfSync,
|
||||
title=_("Values Out Of Sync"),
|
||||
primary_action={
|
||||
"label": _("Make Journal Entry"),
|
||||
"client_action": "erpnext.route_to_adjustment_jv",
|
||||
"args": get_journal_entry(account, stock_adjustment_account, diff),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def get_stock_accounts(company, voucher_type=None, voucher_no=None):
|
||||
stock_accounts = [
|
||||
d.name
|
||||
|
||||
@@ -364,7 +364,7 @@ class Asset(AccountsController):
|
||||
if has_pro_rata and n == 0:
|
||||
# For first entry of monthly depr
|
||||
if r == 0:
|
||||
days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date)
|
||||
days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date) + 1
|
||||
per_day_amt = depreciation_amount / days
|
||||
depreciation_amount_for_current_month = per_day_amt * days_until_first_depr
|
||||
depreciation_amount -= depreciation_amount_for_current_month
|
||||
@@ -628,7 +628,7 @@ class Asset(AccountsController):
|
||||
|
||||
asset_value_after_full_schedule = flt(
|
||||
flt(self.gross_purchase_amount) - flt(accumulated_depreciation_after_full_schedule),
|
||||
self.precision("gross_purchase_amount"),
|
||||
row.precision("expected_value_after_useful_life"),
|
||||
)
|
||||
|
||||
if (
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
"order_confirmation_no",
|
||||
"order_confirmation_date",
|
||||
"amended_from",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"drop_ship",
|
||||
"customer",
|
||||
"customer_name",
|
||||
@@ -1138,16 +1142,39 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Tax Withholding Category",
|
||||
"options": "Tax Withholding Category"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions "
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-28 13:10:47.955401",
|
||||
"modified": "2022-04-26 12:16:38.694276",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -1194,6 +1221,7 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"timeline_field": "supplier",
|
||||
"title_field": "supplier_name",
|
||||
"track_changes": 1
|
||||
|
||||
31
erpnext/change_log/v13/v13_26_0.md
Normal file
31
erpnext/change_log/v13/v13_26_0.md
Normal file
@@ -0,0 +1,31 @@
|
||||
## Version 13.26.0 Release Notes
|
||||
|
||||
### Features & Enhancements
|
||||
|
||||
- feat: Receivable/Payable Account column and filter in AR/AP report ([#30620](https://github.com/frappe/erpnext/pull/30620))
|
||||
- feat(india): e-invoicing for intra-state union territory transactions ([#30626](https://github.com/frappe/erpnext/pull/30626))
|
||||
- feat: Ignore permlevel for specific fields ([#30686](https://github.com/frappe/erpnext/pull/30686))
|
||||
- feat: 'customer' column and more filter to Payment terms status report ([#30499](https://github.com/frappe/erpnext/pull/30499))
|
||||
|
||||
### Fixes
|
||||
|
||||
- fix: fallback to item_name if description is not found ([#30619](https://github.com/frappe/erpnext/pull/30619))
|
||||
- fix: Deferred Revenue/Expense Account validation ([#30602](https://github.com/frappe/erpnext/pull/30602))
|
||||
- fix(pos): reload doc before set value ([#30610](https://github.com/frappe/erpnext/pull/30610))
|
||||
- fix: Exchange gain and loss button in Payment Entry ([#30606](https://github.com/frappe/erpnext/pull/30606))
|
||||
- fix(pos): cannot change paid amount in pos payments ([#30657](https://github.com/frappe/erpnext/pull/30657))
|
||||
- fix: hide pending qty only if original item is assigned ([#30599](https://github.com/frappe/erpnext/pull/30599))
|
||||
- fix(pos): reload doc before set value ([#30611](https://github.com/frappe/erpnext/pull/30611))
|
||||
- fix: Handle multiple item transfer in separate SEs against WO ([#30674](https://github.com/frappe/erpnext/pull/30674))
|
||||
- fix(patch): check null values in is_cancelled patch ([#30594](https://github.com/frappe/erpnext/pull/30594))
|
||||
- fix: Download JSON for GSTR-1 report ([#30651](https://github.com/frappe/erpnext/pull/30651))
|
||||
- fix: remove bad defaults from BOM operation ([#30644](https://github.com/frappe/erpnext/pull/30644))
|
||||
- fix: update translation ([#30474](https://github.com/frappe/erpnext/pull/30474))
|
||||
- fix: dont reassign mutable (list) to a different field ([#30634](https://github.com/frappe/erpnext/pull/30634))
|
||||
- fix: update translation ([#30654](https://github.com/frappe/erpnext/pull/30654))
|
||||
- fix: ignore item-less maintenance visit for sr no ([#30684](https://github.com/frappe/erpnext/pull/30684))
|
||||
- fix: removed unused courses template ([#30596](https://github.com/frappe/erpnext/pull/30596))
|
||||
- fix: Implicit ignore pricing rule check on returns ([#30662](https://github.com/frappe/erpnext/pull/30662))
|
||||
- fix: warehouse naming when suffix is present ([#30621](https://github.com/frappe/erpnext/pull/30621))
|
||||
- fix: Ignore disabled tax categories ([#30542](https://github.com/frappe/erpnext/pull/30542))
|
||||
|
||||
@@ -181,6 +181,7 @@ class AccountsController(TransactionBase):
|
||||
else:
|
||||
self.validate_deferred_start_and_end_date()
|
||||
|
||||
self.validate_deferred_income_expense_account()
|
||||
self.set_inter_company_account()
|
||||
|
||||
if self.doctype == "Purchase Invoice":
|
||||
@@ -209,6 +210,27 @@ class AccountsController(TransactionBase):
|
||||
(self.doctype, self.name),
|
||||
)
|
||||
|
||||
def validate_deferred_income_expense_account(self):
|
||||
field_map = {
|
||||
"Sales Invoice": "deferred_revenue_account",
|
||||
"Purchase Invoice": "deferred_expense_account",
|
||||
}
|
||||
|
||||
for item in self.get("items"):
|
||||
if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"):
|
||||
if not item.get(field_map.get(self.doctype)):
|
||||
default_deferred_account = frappe.db.get_value(
|
||||
"Company", self.company, "default_" + field_map.get(self.doctype)
|
||||
)
|
||||
if not default_deferred_account:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Please update deferred revenue/expense account in item row or default account in company master"
|
||||
).format(item.idx)
|
||||
)
|
||||
else:
|
||||
item.set(field_map.get(self.doctype), default_deferred_account)
|
||||
|
||||
def validate_deferred_start_and_end_date(self):
|
||||
for d in self.items:
|
||||
if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"):
|
||||
@@ -1978,12 +2000,13 @@ def get_advance_journal_entries(
|
||||
|
||||
reference_condition = " and (" + " or ".join(conditions) + ")" if conditions else ""
|
||||
|
||||
# nosemgrep
|
||||
journal_entries = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Journal Entry" as reference_type, t1.name as reference_name,
|
||||
t1.remark as remarks, t2.{0} as amount, t2.name as reference_row,
|
||||
t2.reference_name as against_order
|
||||
t2.reference_name as against_order, t2.exchange_rate
|
||||
from
|
||||
`tabJournal Entry` t1, `tabJournal Entry Account` t2
|
||||
where
|
||||
|
||||
@@ -22,6 +22,9 @@ class QtyMismatchError(ValidationError):
|
||||
|
||||
|
||||
class BuyingController(StockController, Subcontracting):
|
||||
def __setup__(self):
|
||||
self.flags.ignore_permlevel_for_fields = ["buying_price_list", "price_list_currency"]
|
||||
|
||||
def get_feed(self):
|
||||
if self.get("supplier_name"):
|
||||
return _("From {0} | {1} {2}").format(self.supplier_name, self.currency, self.grand_total)
|
||||
|
||||
@@ -330,7 +330,6 @@ def make_return_doc(doctype, source_name, target_doc=None):
|
||||
doc = frappe.get_doc(target)
|
||||
doc.is_return = 1
|
||||
doc.return_against = source.name
|
||||
doc.ignore_pricing_rule = 1
|
||||
doc.set_warehouse = ""
|
||||
if doctype == "Sales Invoice" or doctype == "POS Invoice":
|
||||
doc.is_pos = source.is_pos
|
||||
|
||||
@@ -16,6 +16,9 @@ from erpnext.stock.utils import get_incoming_rate
|
||||
|
||||
|
||||
class SellingController(StockController):
|
||||
def __setup__(self):
|
||||
self.flags.ignore_permlevel_for_fields = ["selling_price_list", "price_list_currency"]
|
||||
|
||||
def get_feed(self):
|
||||
return _("To {0} | {1} {2}").format(self.customer_name, self.currency, self.grand_total)
|
||||
|
||||
|
||||
@@ -24,17 +24,17 @@ frappe.ui.form.on("E Commerce Settings", {
|
||||
);
|
||||
}
|
||||
|
||||
frappe.model.with_doctype("Item", () => {
|
||||
frappe.model.with_doctype("Website Item", () => {
|
||||
const web_item_meta = frappe.get_meta('Website Item');
|
||||
|
||||
const valid_fields = web_item_meta.fields.filter(
|
||||
df => ["Link", "Table MultiSelect"].includes(df.fieldtype) && !df.hidden
|
||||
).map(df => ({ label: df.label, value: df.fieldname }));
|
||||
|
||||
frm.fields_dict.filter_fields.grid.update_docfield_property(
|
||||
frm.get_field("filter_fields").grid.update_docfield_property(
|
||||
'fieldname', 'fieldtype', 'Select'
|
||||
);
|
||||
frm.fields_dict.filter_fields.grid.update_docfield_property(
|
||||
frm.get_field("filter_fields").grid.update_docfield_property(
|
||||
'fieldname', 'options', valid_fields
|
||||
);
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ class ECommerceSettings(Document):
|
||||
self.is_redisearch_loaded = is_search_module_loaded()
|
||||
|
||||
def validate(self):
|
||||
self.validate_field_filters()
|
||||
self.validate_field_filters(self.filter_fields, self.enable_field_filters)
|
||||
self.validate_attribute_filters()
|
||||
self.validate_checkout()
|
||||
self.validate_search_index_fields()
|
||||
@@ -50,21 +50,22 @@ class ECommerceSettings(Document):
|
||||
define_autocomplete_dictionary()
|
||||
create_website_items_index()
|
||||
|
||||
def validate_field_filters(self):
|
||||
if not (self.enable_field_filters and self.filter_fields):
|
||||
@staticmethod
|
||||
def validate_field_filters(filter_fields, enable_field_filters):
|
||||
if not (enable_field_filters and filter_fields):
|
||||
return
|
||||
|
||||
item_meta = frappe.get_meta("Item")
|
||||
web_item_meta = frappe.get_meta("Website Item")
|
||||
valid_fields = [
|
||||
df.fieldname for df in item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]
|
||||
df.fieldname for df in web_item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]
|
||||
]
|
||||
|
||||
for f in self.filter_fields:
|
||||
if f.fieldname not in valid_fields:
|
||||
for row in filter_fields:
|
||||
if row.fieldname not in valid_fields:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Filter Fields Row #{0}: Fieldname <b>{1}</b> must be of type 'Link' or 'Table MultiSelect'"
|
||||
).format(f.idx, f.fieldname)
|
||||
"Filter Fields Row #{0}: Fieldname {1} must be of type 'Link' or 'Table MultiSelect'"
|
||||
).format(row.idx, frappe.bold(row.fieldname))
|
||||
)
|
||||
|
||||
def validate_attribute_filters(self):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
@@ -11,42 +11,34 @@ from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
||||
|
||||
|
||||
class TestECommerceSettings(unittest.TestCase):
|
||||
def setUp(self):
|
||||
frappe.db.sql("""delete from `tabSingles` where doctype="Shipping Cart Settings" """)
|
||||
|
||||
def get_cart_settings(self):
|
||||
return frappe.get_doc({"doctype": "E Commerce Settings", "company": "_Test Company"})
|
||||
|
||||
# NOTE: Exchangrate API has all enabled currencies that ERPNext supports.
|
||||
# We aren't checking just currency exchange record anymore
|
||||
# while validating price list currency exchange rate to that of company.
|
||||
# The API is being used to fetch the rate which again almost always
|
||||
# gives back a valid value (for valid currencies).
|
||||
# This makes the test obsolete.
|
||||
# Commenting because im not sure if there's a better test we can write
|
||||
|
||||
# def test_exchange_rate_exists(self):
|
||||
# frappe.db.sql("""delete from `tabCurrency Exchange`""")
|
||||
|
||||
# cart_settings = self.get_cart_settings()
|
||||
# cart_settings.price_list = "_Test Price List Rest of the World"
|
||||
# self.assertRaises(ShoppingCartSetupError, cart_settings.validate_price_list_exchange_rate)
|
||||
|
||||
# from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records as \
|
||||
# currency_exchange_records
|
||||
# frappe.get_doc(currency_exchange_records[0]).insert()
|
||||
# cart_settings.validate_price_list_exchange_rate()
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_tax_rule_validation(self):
|
||||
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0")
|
||||
|
||||
cart_settings = self.get_cart_settings()
|
||||
cart_settings = frappe.get_doc("E Commerce Settings")
|
||||
cart_settings.enabled = 1
|
||||
if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1}, "name"):
|
||||
self.assertRaises(ShoppingCartSetupError, cart_settings.validate_tax_rule)
|
||||
|
||||
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 1")
|
||||
|
||||
def test_invalid_filter_fields(self):
|
||||
"Check if Item fields are blocked in E Commerce Settings filter fields."
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
|
||||
setup_e_commerce_settings({"enable_field_filters": 1})
|
||||
|
||||
create_custom_field(
|
||||
"Item",
|
||||
dict(owner="Administrator", fieldname="test_data", label="Test", fieldtype="Data"),
|
||||
)
|
||||
settings = frappe.get_doc("E Commerce Settings")
|
||||
settings.append("filter_fields", {"fieldname": "test_data"})
|
||||
|
||||
self.assertRaises(frappe.ValidationError, settings.save)
|
||||
|
||||
|
||||
def setup_e_commerce_settings(values_dict):
|
||||
"Accepts a dict of values that updates E Commerce Settings."
|
||||
|
||||
@@ -22,12 +22,14 @@ class ProductFiltersBuilder:
|
||||
fields, filter_data = [], []
|
||||
filter_fields = [row.fieldname for row in self.doc.filter_fields] # fields in settings
|
||||
|
||||
# filter valid field filters i.e. those that exist in Item
|
||||
item_meta = frappe.get_meta("Item", cached=True)
|
||||
fields = [item_meta.get_field(field) for field in filter_fields if item_meta.has_field(field)]
|
||||
# filter valid field filters i.e. those that exist in Website Item
|
||||
web_item_meta = frappe.get_meta("Website Item", cached=True)
|
||||
fields = [
|
||||
web_item_meta.get_field(field) for field in filter_fields if web_item_meta.has_field(field)
|
||||
]
|
||||
|
||||
for df in fields:
|
||||
item_filters, item_or_filters = {"published_in_website": 1}, []
|
||||
item_filters, item_or_filters = {"published": 1}, []
|
||||
link_doctype_values = self.get_filtered_link_doctype_records(df)
|
||||
|
||||
if df.fieldtype == "Link":
|
||||
@@ -50,9 +52,13 @@ class ProductFiltersBuilder:
|
||||
]
|
||||
)
|
||||
|
||||
# exclude variants if mentioned in settings
|
||||
if frappe.db.get_single_value("E Commerce Settings", "hide_variants"):
|
||||
item_filters["variant_of"] = ["is", "not set"]
|
||||
|
||||
# Get link field values attached to published items
|
||||
item_values = frappe.get_all(
|
||||
"Item",
|
||||
"Website Item",
|
||||
fields=[df.fieldname],
|
||||
filters=item_filters,
|
||||
or_filters=item_or_filters,
|
||||
|
||||
@@ -277,6 +277,54 @@ class TestProductDataEngine(unittest.TestCase):
|
||||
# tear down
|
||||
setup_e_commerce_settings({"enable_attribute_filters": 1, "hide_variants": 0})
|
||||
|
||||
def test_custom_field_as_filter(self):
|
||||
"Test if custom field functions as filter correctly."
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
|
||||
create_custom_field(
|
||||
"Website Item",
|
||||
dict(
|
||||
owner="Administrator",
|
||||
fieldname="supplier",
|
||||
label="Supplier",
|
||||
fieldtype="Link",
|
||||
options="Supplier",
|
||||
insert_after="on_backorder",
|
||||
),
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Website Item", {"item_code": "Test 11I Laptop"}, "supplier", "_Test Supplier"
|
||||
)
|
||||
frappe.db.set_value(
|
||||
"Website Item", {"item_code": "Test 12I Laptop"}, "supplier", "_Test Supplier 1"
|
||||
)
|
||||
|
||||
settings = frappe.get_doc("E Commerce Settings")
|
||||
settings.append("filter_fields", {"fieldname": "supplier"})
|
||||
settings.save()
|
||||
|
||||
filter_engine = ProductFiltersBuilder()
|
||||
field_filters = filter_engine.get_field_filters()
|
||||
custom_filter = field_filters[1]
|
||||
filter_values = custom_filter[1]
|
||||
|
||||
self.assertEqual(custom_filter[0].options, "Supplier")
|
||||
self.assertEqual(len(filter_values), 2)
|
||||
self.assertIn("_Test Supplier", filter_values)
|
||||
|
||||
# test if custom filter works in query
|
||||
field_filters = {"supplier": "_Test Supplier 1"}
|
||||
engine = ProductQuery()
|
||||
result = engine.query(
|
||||
attributes={}, fields=field_filters, search_term=None, start=0, item_group=None
|
||||
)
|
||||
items = result.get("items")
|
||||
|
||||
# check if only 'Raw Material' are fetched in the right order
|
||||
self.assertEqual(len(items), 1)
|
||||
self.assertEqual(items[0].get("item_code"), "Test 12I Laptop")
|
||||
|
||||
|
||||
def create_variant_web_item():
|
||||
"Create Variant and Template Website Items."
|
||||
|
||||
@@ -37,7 +37,7 @@ frappe.ui.form.on("Shopify Settings", "refresh", function(frm){
|
||||
|
||||
}
|
||||
|
||||
let app_link = "<a href='https://frappecloud.com/marketplace/apps/ecommerce-integrations' target='_blank'>Ecommerce Integrations</a>"
|
||||
let app_link = "<a href='https://frappecloud.com/marketplace/apps/ecommerce_integrations' target='_blank'>Ecommerce Integrations</a>"
|
||||
frm.dashboard.add_comment(__("Shopify Integration will be removed from ERPNext in Version 14. Please install {0} app to continue using it.", [app_link]), "yellow", true);
|
||||
|
||||
})
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:healthcare_service_unit_name",
|
||||
"beta": 1,
|
||||
"creation": "2016-09-21 13:48:14.731437",
|
||||
"description": "Healthcare Service Unit",
|
||||
@@ -207,7 +206,7 @@
|
||||
],
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-19 14:09:11.643464",
|
||||
"modified": "2022-04-07 03:11:36.023277",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Healthcare",
|
||||
"name": "Healthcare Service Unit",
|
||||
|
||||
@@ -592,6 +592,9 @@ accounting_dimension_doctypes = [
|
||||
"Subscription Plan",
|
||||
"POS Invoice",
|
||||
"POS Invoice Item",
|
||||
"Purchase Order",
|
||||
"Purchase Receipt",
|
||||
"Sales Order",
|
||||
]
|
||||
|
||||
regional_overrides = {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, get_datetime
|
||||
from frappe.utils import cint, get_datetime, get_link_to_form
|
||||
|
||||
from erpnext.hr.doctype.shift_assignment.shift_assignment import (
|
||||
get_actual_start_end_datetime_of_shift,
|
||||
@@ -127,19 +127,17 @@ def mark_attendance_and_link_log(
|
||||
log_names = [x.name for x in logs]
|
||||
employee = logs[0].employee
|
||||
if attendance_status == "Skip":
|
||||
frappe.db.sql(
|
||||
"""update `tabEmployee Checkin`
|
||||
set skip_auto_attendance = %s
|
||||
where name in %s""",
|
||||
("1", log_names),
|
||||
)
|
||||
skip_attendance_in_checkins(log_names)
|
||||
return None
|
||||
|
||||
elif attendance_status in ("Present", "Absent", "Half Day"):
|
||||
employee_doc = frappe.get_doc("Employee", employee)
|
||||
if not frappe.db.exists(
|
||||
duplicate = frappe.db.exists(
|
||||
"Attendance",
|
||||
{"employee": employee, "attendance_date": attendance_date, "docstatus": ("!=", "2")},
|
||||
):
|
||||
)
|
||||
|
||||
if not duplicate:
|
||||
doc_dict = {
|
||||
"doctype": "Attendance",
|
||||
"employee": employee,
|
||||
@@ -155,6 +153,12 @@ def mark_attendance_and_link_log(
|
||||
}
|
||||
attendance = frappe.get_doc(doc_dict).insert()
|
||||
attendance.submit()
|
||||
|
||||
if attendance_status == "Absent":
|
||||
attendance.add_comment(
|
||||
text=_("Employee was marked Absent for not meeting the working hours threshold.")
|
||||
)
|
||||
|
||||
frappe.db.sql(
|
||||
"""update `tabEmployee Checkin`
|
||||
set attendance = %s
|
||||
@@ -163,12 +167,10 @@ def mark_attendance_and_link_log(
|
||||
)
|
||||
return attendance
|
||||
else:
|
||||
frappe.db.sql(
|
||||
"""update `tabEmployee Checkin`
|
||||
set skip_auto_attendance = %s
|
||||
where name in %s""",
|
||||
("1", log_names),
|
||||
)
|
||||
skip_attendance_in_checkins(log_names)
|
||||
if duplicate:
|
||||
add_comment_in_checkins(log_names, duplicate)
|
||||
|
||||
return None
|
||||
else:
|
||||
frappe.throw(_("{} is an invalid Attendance Status.").format(attendance_status))
|
||||
@@ -237,3 +239,29 @@ def time_diff_in_hours(start, end):
|
||||
|
||||
def find_index_in_dict(dict_list, key, value):
|
||||
return next((index for (index, d) in enumerate(dict_list) if d[key] == value), None)
|
||||
|
||||
|
||||
def add_comment_in_checkins(log_names, duplicate):
|
||||
text = _("Auto Attendance skipped due to duplicate attendance record: {}").format(
|
||||
get_link_to_form("Attendance", duplicate)
|
||||
)
|
||||
|
||||
for name in log_names:
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Comment",
|
||||
"comment_type": "Comment",
|
||||
"reference_doctype": "Employee Checkin",
|
||||
"reference_name": name,
|
||||
"content": text,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def skip_attendance_in_checkins(log_names):
|
||||
EmployeeCheckin = frappe.qb.DocType("Employee Checkin")
|
||||
(
|
||||
frappe.qb.update(EmployeeCheckin)
|
||||
.set("skip_auto_attendance", 1)
|
||||
.where(EmployeeCheckin.name.isin(log_names))
|
||||
).run()
|
||||
|
||||
@@ -34,6 +34,15 @@ frappe.ui.form.on("Leave Allocation", {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// make new leaves allocated field read only if allocation is created via leave policy assignment
|
||||
// and leave type is earned leave, since these leaves would be allocated via the scheduler
|
||||
if (frm.doc.leave_policy_assignment) {
|
||||
frappe.db.get_value("Leave Type", frm.doc.leave_type, "is_earned_leave", (r) => {
|
||||
if (r && cint(r.is_earned_leave))
|
||||
frm.set_df_property("new_leaves_allocated", "read_only", 1);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
expire_allocation: function(frm) {
|
||||
|
||||
@@ -237,7 +237,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-01 15:28:26.335104",
|
||||
"modified": "2022-04-07 09:50:33.145825",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Leave Allocation",
|
||||
@@ -278,5 +278,7 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"timeline_field": "employee"
|
||||
"timeline_field": "employee",
|
||||
"title_field": "employee_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ class ShiftAssignment(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_events(start, end, filters=None):
|
||||
events = []
|
||||
from frappe.desk.calendar import get_event_conditions
|
||||
|
||||
employee = frappe.db.get_value(
|
||||
"Employee", {"user_id": frappe.session.user}, ["name", "company"], as_dict=True
|
||||
@@ -95,20 +95,22 @@ def get_events(start, end, filters=None):
|
||||
employee = ""
|
||||
company = frappe.db.get_value("Global Defaults", None, "default_company")
|
||||
|
||||
from frappe.desk.reportview import get_filters_cond
|
||||
|
||||
conditions = get_filters_cond("Shift Assignment", filters, [])
|
||||
add_assignments(events, start, end, conditions=conditions)
|
||||
conditions = get_event_conditions("Shift Assignment", filters)
|
||||
events = add_assignments(start, end, conditions=conditions)
|
||||
return events
|
||||
|
||||
|
||||
def add_assignments(events, start, end, conditions=None):
|
||||
def add_assignments(start, end, conditions=None):
|
||||
events = []
|
||||
|
||||
query = """select name, start_date, end_date, employee_name,
|
||||
employee, docstatus, shift_type
|
||||
from `tabShift Assignment` where
|
||||
start_date >= %(start_date)s
|
||||
or end_date <= %(end_date)s
|
||||
or (%(start_date)s between start_date and end_date and %(end_date)s between start_date and end_date)
|
||||
(
|
||||
start_date >= %(start_date)s
|
||||
or end_date <= %(end_date)s
|
||||
or (%(start_date)s between start_date and end_date and %(end_date)s between start_date and end_date)
|
||||
)
|
||||
and docstatus = 1"""
|
||||
if conditions:
|
||||
query += conditions
|
||||
|
||||
@@ -4,14 +4,22 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, nowdate
|
||||
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.hr.doctype.shift_assignment.shift_assignment import get_events
|
||||
|
||||
test_dependencies = ["Shift Type"]
|
||||
|
||||
|
||||
class TestShiftAssignment(unittest.TestCase):
|
||||
class TestShiftAssignment(FrappeTestCase):
|
||||
def setUp(self):
|
||||
frappe.db.sql("delete from `tabShift Assignment`")
|
||||
frappe.db.delete("Shift Assignment")
|
||||
if not frappe.db.exists("Shift Type", "Day Shift"):
|
||||
frappe.get_doc(
|
||||
{"doctype": "Shift Type", "name": "Day Shift", "start_time": "9:00:00", "end_time": "18:00:00"}
|
||||
).insert()
|
||||
|
||||
def test_make_shift_assignment(self):
|
||||
shift_assignment = frappe.get_doc(
|
||||
@@ -86,3 +94,36 @@ class TestShiftAssignment(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, shift_assignment_3.save)
|
||||
|
||||
def test_shift_assignment_calendar(self):
|
||||
employee1 = make_employee("test_shift_assignment1@example.com", company="_Test Company")
|
||||
employee2 = make_employee("test_shift_assignment2@example.com", company="_Test Company")
|
||||
date = nowdate()
|
||||
|
||||
shift_1 = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Shift Assignment",
|
||||
"shift_type": "Day Shift",
|
||||
"company": "_Test Company",
|
||||
"employee": employee1,
|
||||
"start_date": date,
|
||||
"status": "Active",
|
||||
}
|
||||
).submit()
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Shift Assignment",
|
||||
"shift_type": "Day Shift",
|
||||
"company": "_Test Company",
|
||||
"employee": employee2,
|
||||
"start_date": date,
|
||||
"status": "Active",
|
||||
}
|
||||
).submit()
|
||||
|
||||
events = get_events(
|
||||
start=date, end=date, filters=[["Shift Assignment", "employee", "=", employee1, False]]
|
||||
)
|
||||
self.assertEqual(len(events), 1)
|
||||
self.assertEqual(events[0]["name"], shift_1.name)
|
||||
|
||||
@@ -139,7 +139,17 @@ class ShiftType(Document):
|
||||
for date in dates:
|
||||
shift_details = get_employee_shift(employee, date, True)
|
||||
if shift_details and shift_details.shift_type.name == self.name:
|
||||
mark_attendance(employee, date, "Absent", self.name)
|
||||
attendance = mark_attendance(employee, date, "Absent", self.name)
|
||||
if attendance:
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Comment",
|
||||
"comment_type": "Comment",
|
||||
"reference_doctype": "Attendance",
|
||||
"reference_name": attendance,
|
||||
"content": frappe._("Employee was marked Absent due to missing Employee Checkins."),
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
def get_assigned_employee(self, from_date=None, consider_default_shift=False):
|
||||
filters = {"start_date": (">", from_date), "shift_type": self.name, "docstatus": "1"}
|
||||
|
||||
@@ -57,11 +57,10 @@ def execute(filters=None):
|
||||
|
||||
data = []
|
||||
|
||||
leave_list = None
|
||||
leave_types = None
|
||||
if filters.summarized_view:
|
||||
leave_types = frappe.db.sql("""select name from `tabLeave Type`""", as_list=True)
|
||||
leave_list = [d[0] + ":Float:120" for d in leave_types]
|
||||
columns.extend(leave_list)
|
||||
leave_types = frappe.get_all("Leave Type", pluck="name")
|
||||
columns.extend([leave_type + ":Float:120" for leave_type in leave_types])
|
||||
columns.extend([_("Total Late Entries") + ":Float:120", _("Total Early Exits") + ":Float:120"])
|
||||
|
||||
if filters.group_by:
|
||||
@@ -81,13 +80,19 @@ def execute(filters=None):
|
||||
holiday_map,
|
||||
conditions,
|
||||
default_holiday_list,
|
||||
leave_list=leave_list,
|
||||
leave_types=leave_types,
|
||||
)
|
||||
emp_att_map.update(emp_att_data)
|
||||
data += record
|
||||
else:
|
||||
record, emp_att_map = add_data(
|
||||
emp_map, att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=leave_list
|
||||
emp_map,
|
||||
att_map,
|
||||
filters,
|
||||
holiday_map,
|
||||
conditions,
|
||||
default_holiday_list,
|
||||
leave_types=leave_types,
|
||||
)
|
||||
data += record
|
||||
|
||||
@@ -104,12 +109,10 @@ def get_chart_data(emp_att_map, days):
|
||||
{"name": "Leave", "values": []},
|
||||
]
|
||||
for idx, day in enumerate(days, start=0):
|
||||
p = day.replace("::65", "")
|
||||
labels.append(day.replace("::65", ""))
|
||||
total_absent_on_day = 0
|
||||
total_leave_on_day = 0
|
||||
total_present_on_day = 0
|
||||
total_holiday = 0
|
||||
for emp in emp_att_map.keys():
|
||||
if emp_att_map[emp][idx]:
|
||||
if emp_att_map[emp][idx] == "A":
|
||||
@@ -134,9 +137,8 @@ def get_chart_data(emp_att_map, days):
|
||||
|
||||
|
||||
def add_data(
|
||||
employee_map, att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=None
|
||||
employee_map, att_map, filters, holiday_map, conditions, default_holiday_list, leave_types=None
|
||||
):
|
||||
|
||||
record = []
|
||||
emp_att_map = {}
|
||||
for emp in employee_map:
|
||||
@@ -222,7 +224,7 @@ def add_data(
|
||||
else:
|
||||
leaves[d.leave_type] = d.count
|
||||
|
||||
for d in leave_list:
|
||||
for d in leave_types:
|
||||
if d in leaves:
|
||||
row.append(leaves[d])
|
||||
else:
|
||||
|
||||
@@ -489,6 +489,17 @@ def update_previous_leave_allocation(
|
||||
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
|
||||
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
|
||||
|
||||
if e_leave_type.based_on_date_of_joining:
|
||||
text = _("allocated {0} leave(s) via scheduler on {1} based on the date of joining").format(
|
||||
frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
|
||||
)
|
||||
else:
|
||||
text = _("allocated {0} leave(s) via scheduler on {1}").format(
|
||||
frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
|
||||
)
|
||||
|
||||
allocation.add_comment(comment_type="Info", text=text)
|
||||
|
||||
|
||||
def get_monthly_earned_leave(annual_leaves, frequency, rounding):
|
||||
earned_leaves = 0.0
|
||||
|
||||
@@ -12,6 +12,9 @@ frappe.ui.form.on('Maintenance Visit', {
|
||||
// filters for serial no based on item code
|
||||
if (frm.doc.maintenance_type === "Scheduled") {
|
||||
let item_code = frm.doc.purposes[0].item_code;
|
||||
if (!item_code) {
|
||||
return;
|
||||
}
|
||||
frappe.call({
|
||||
method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.get_serial_nos_from_schedule",
|
||||
args: {
|
||||
|
||||
@@ -100,7 +100,6 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "5",
|
||||
"depends_on": "eval:parent.doctype == 'BOM'",
|
||||
"fieldname": "base_operating_cost",
|
||||
"fieldtype": "Currency",
|
||||
@@ -178,7 +177,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-03-10 06:19:08.462027",
|
||||
"modified": "2022-04-08 01:18:33.547481",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM Operation",
|
||||
|
||||
@@ -28,12 +28,12 @@ frappe.ui.form.on('Job Card', {
|
||||
frappe.flags.resume_job = 0;
|
||||
let has_items = frm.doc.items && frm.doc.items.length;
|
||||
|
||||
if (frm.doc.__onload.work_order_stopped) {
|
||||
if (!frm.is_new() && frm.doc.__onload.work_order_stopped) {
|
||||
frm.disable_save();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!frm.doc.__islocal && has_items && frm.doc.docstatus < 2) {
|
||||
if (!frm.is_new() && has_items && frm.doc.docstatus < 2) {
|
||||
let to_request = frm.doc.for_quantity > frm.doc.transferred_qty;
|
||||
let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer;
|
||||
|
||||
@@ -73,7 +73,18 @@ frappe.ui.form.on('Job Card', {
|
||||
if (frm.doc.docstatus == 0 && !frm.is_new() &&
|
||||
(frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity)
|
||||
&& (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
|
||||
frm.trigger("prepare_timer_buttons");
|
||||
|
||||
// if Job Card is link to Work Order, the job card must not be able to start if Work Order not "Started"
|
||||
// and if stock mvt for WIP is required
|
||||
if (frm.doc.work_order) {
|
||||
frappe.db.get_value('Work Order', frm.doc.work_order, ['skip_transfer', 'status'], (result) => {
|
||||
if (result.skip_transfer === 1 || result.status == 'In Process') {
|
||||
frm.trigger("prepare_timer_buttons");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
frm.trigger("prepare_timer_buttons");
|
||||
}
|
||||
}
|
||||
frm.trigger("setup_quality_inspection");
|
||||
|
||||
|
||||
@@ -462,6 +462,7 @@ class ProductionPlan(Document):
|
||||
work_order_data = {
|
||||
"wip_warehouse": default_warehouses.get("wip_warehouse"),
|
||||
"fg_warehouse": default_warehouses.get("fg_warehouse"),
|
||||
"company": self.get("company"),
|
||||
}
|
||||
|
||||
self.prepare_data_for_sub_assembly_items(row, work_order_data)
|
||||
@@ -499,6 +500,7 @@ class ProductionPlan(Document):
|
||||
|
||||
for supplier, po_list in subcontracted_po.items():
|
||||
po = frappe.new_doc("Purchase Order")
|
||||
po.company = self.company
|
||||
po.supplier = supplier
|
||||
po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate()
|
||||
po.is_subcontracted = "Yes"
|
||||
|
||||
@@ -1100,6 +1100,56 @@ class TestWorkOrder(FrappeTestCase):
|
||||
for index, row in enumerate(ste_manu.get("items"), start=1):
|
||||
self.assertEqual(index, row.idx)
|
||||
|
||||
@change_settings(
|
||||
"Manufacturing Settings",
|
||||
{"backflush_raw_materials_based_on": "Material Transferred for Manufacture"},
|
||||
)
|
||||
def test_work_order_multiple_material_transfer(self):
|
||||
"""
|
||||
Test transferring multiple RMs in separate Stock Entries.
|
||||
"""
|
||||
work_order = make_wo_order_test_record(planned_start_date=now(), qty=1)
|
||||
test_stock_entry.make_stock_entry( # stock up RM
|
||||
item_code="_Test Item",
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=1,
|
||||
basic_rate=5000.0,
|
||||
)
|
||||
test_stock_entry.make_stock_entry( # stock up RM
|
||||
item_code="_Test Item Home Desktop 100",
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=2,
|
||||
basic_rate=1000.0,
|
||||
)
|
||||
|
||||
transfer_entry = frappe.get_doc(
|
||||
make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1)
|
||||
)
|
||||
del transfer_entry.get("items")[0] # transfer only one RM
|
||||
transfer_entry.submit()
|
||||
|
||||
# WO's "Material Transferred for Mfg" shows all is transferred, one RM is pending
|
||||
work_order.reload()
|
||||
self.assertEqual(work_order.material_transferred_for_manufacturing, 1)
|
||||
self.assertEqual(work_order.required_items[0].transferred_qty, 0)
|
||||
self.assertEqual(work_order.required_items[1].transferred_qty, 2)
|
||||
|
||||
final_transfer_entry = frappe.get_doc( # transfer last RM with For Quantity = 0
|
||||
make_stock_entry(work_order.name, "Material Transfer for Manufacture", 0)
|
||||
)
|
||||
final_transfer_entry.save()
|
||||
|
||||
self.assertEqual(final_transfer_entry.fg_completed_qty, 0.0)
|
||||
self.assertEqual(final_transfer_entry.items[0].qty, 1)
|
||||
|
||||
final_transfer_entry.submit()
|
||||
work_order.reload()
|
||||
|
||||
# WO's "Material Transferred for Mfg" shows all is transferred, no RM is pending
|
||||
self.assertEqual(work_order.material_transferred_for_manufacturing, 1)
|
||||
self.assertEqual(work_order.required_items[0].transferred_qty, 1)
|
||||
self.assertEqual(work_order.required_items[1].transferred_qty, 2)
|
||||
|
||||
|
||||
def update_job_card(job_card, jc_qty=None):
|
||||
employee = frappe.db.get_value("Employee", {"status": "Active"}, "name")
|
||||
|
||||
@@ -540,8 +540,10 @@ erpnext.work_order = {
|
||||
|| frm.doc.transfer_material_against == 'Job Card') ? 0 : 1;
|
||||
|
||||
if (show_start_btn) {
|
||||
if ((flt(doc.material_transferred_for_manufacturing) < flt(doc.qty))
|
||||
&& frm.doc.status != 'Stopped') {
|
||||
let pending_to_transfer = frm.doc.required_items.some(
|
||||
item => flt(item.transferred_qty) < flt(item.required_qty)
|
||||
);
|
||||
if (pending_to_transfer && frm.doc.status != 'Stopped') {
|
||||
frm.has_start_btn = true;
|
||||
frm.add_custom_button(__('Create Pick List'), function() {
|
||||
erpnext.work_order.create_pick_list(frm);
|
||||
|
||||
@@ -1174,7 +1174,11 @@ def make_stock_entry(work_order_id, purpose, qty=None):
|
||||
stock_entry.from_bom = 1
|
||||
stock_entry.bom_no = work_order.bom_no
|
||||
stock_entry.use_multi_level_bom = work_order.use_multi_level_bom
|
||||
stock_entry.fg_completed_qty = qty or (flt(work_order.qty) - flt(work_order.produced_qty))
|
||||
# accept 0 qty as well
|
||||
stock_entry.fg_completed_qty = (
|
||||
qty if qty is not None else (flt(work_order.qty) - flt(work_order.produced_qty))
|
||||
)
|
||||
|
||||
if work_order.bom_no:
|
||||
stock_entry.inspection_required = frappe.db.get_value(
|
||||
"BOM", work_order.bom_no, "inspection_required"
|
||||
|
||||
@@ -350,6 +350,7 @@ erpnext.patches.v13_0.enable_provisional_accounting
|
||||
erpnext.patches.v13_0.update_disbursement_account
|
||||
erpnext.patches.v13_0.update_reserved_qty_closed_wo
|
||||
erpnext.patches.v13_0.amazon_mws_deprecation_warning
|
||||
erpnext.patches.v13_0.datev_deprecation_warning
|
||||
erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr
|
||||
erpnext.patches.v13_0.update_accounts_in_loan_docs
|
||||
erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022
|
||||
@@ -357,4 +358,8 @@ erpnext.patches.v13_0.rename_non_profit_fields
|
||||
erpnext.patches.v13_0.enable_ksa_vat_docs #1
|
||||
erpnext.patches.v13_0.create_gst_custom_fields_in_quotation
|
||||
erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances
|
||||
erpnext.patches.v13_0.change_default_item_manufacturer_fieldtype
|
||||
erpnext.patches.v13_0.set_return_against_in_pos_invoice_references
|
||||
erpnext.patches.v13_0.copy_custom_field_filters_to_website_item
|
||||
erpnext.patches.v13_0.education_deprecation_warning
|
||||
erpnext.patches.v13_0.create_accounting_dimensions_in_orders
|
||||
|
||||
@@ -20,7 +20,7 @@ def execute():
|
||||
"""
|
||||
UPDATE `tab{doctype}`
|
||||
SET is_cancelled = 0
|
||||
where is_cancelled in ('', NULL, 'No')""".format(
|
||||
where is_cancelled in ('', 'No') or is_cancelled is NULL""".format(
|
||||
doctype=doctype
|
||||
)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
|
||||
# Erase all default item manufacturers that dont exist.
|
||||
item = frappe.qb.DocType("Item")
|
||||
manufacturer = frappe.qb.DocType("Manufacturer")
|
||||
|
||||
(
|
||||
frappe.qb.update(item)
|
||||
.set(item.default_item_manufacturer, None)
|
||||
.left_join(manufacturer)
|
||||
.on(item.default_item_manufacturer == manufacturer.name)
|
||||
.where(manufacturer.name.isnull() & item.default_item_manufacturer.isnotnull())
|
||||
).run()
|
||||
@@ -0,0 +1,94 @@
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
|
||||
|
||||
def execute():
|
||||
"Add Field Filters, that are not standard fields in Website Item, as Custom Fields."
|
||||
|
||||
def move_table_multiselect_data(docfield):
|
||||
"Copy child table data (Table Multiselect) from Item to Website Item for a docfield."
|
||||
table_multiselect_data = get_table_multiselect_data(docfield)
|
||||
field = docfield.fieldname
|
||||
|
||||
for row in table_multiselect_data:
|
||||
# add copied multiselect data rows in Website Item
|
||||
web_item = frappe.db.get_value("Website Item", {"item_code": row.parent})
|
||||
web_item_doc = frappe.get_doc("Website Item", web_item)
|
||||
|
||||
child_doc = frappe.new_doc(docfield.options, web_item_doc, field)
|
||||
|
||||
for field in ["name", "creation", "modified", "idx"]:
|
||||
row[field] = None
|
||||
|
||||
child_doc.update(row)
|
||||
|
||||
child_doc.parenttype = "Website Item"
|
||||
child_doc.parent = web_item
|
||||
|
||||
child_doc.insert()
|
||||
|
||||
def get_table_multiselect_data(docfield):
|
||||
child_table = frappe.qb.DocType(docfield.options)
|
||||
item = frappe.qb.DocType("Item")
|
||||
|
||||
table_multiselect_data = ( # query table data for field
|
||||
frappe.qb.from_(child_table)
|
||||
.join(item)
|
||||
.on(item.item_code == child_table.parent)
|
||||
.select(child_table.star)
|
||||
.where((child_table.parentfield == docfield.fieldname) & (item.published_in_website == 1))
|
||||
).run(as_dict=True)
|
||||
|
||||
return table_multiselect_data
|
||||
|
||||
settings = frappe.get_doc("E Commerce Settings")
|
||||
|
||||
if not (settings.enable_field_filters or settings.filter_fields):
|
||||
return
|
||||
|
||||
item_meta = frappe.get_meta("Item")
|
||||
valid_item_fields = [
|
||||
df.fieldname for df in item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]
|
||||
]
|
||||
|
||||
web_item_meta = frappe.get_meta("Website Item")
|
||||
valid_web_item_fields = [
|
||||
df.fieldname for df in web_item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]
|
||||
]
|
||||
|
||||
for row in settings.filter_fields:
|
||||
# skip if illegal field
|
||||
if row.fieldname not in valid_item_fields:
|
||||
continue
|
||||
|
||||
# if Item field is not in Website Item, add it as a custom field
|
||||
if row.fieldname not in valid_web_item_fields:
|
||||
df = item_meta.get_field(row.fieldname)
|
||||
create_custom_field(
|
||||
"Website Item",
|
||||
dict(
|
||||
owner="Administrator",
|
||||
fieldname=df.fieldname,
|
||||
label=df.label,
|
||||
fieldtype=df.fieldtype,
|
||||
options=df.options,
|
||||
description=df.description,
|
||||
read_only=df.read_only,
|
||||
no_copy=df.no_copy,
|
||||
insert_after="on_backorder",
|
||||
),
|
||||
)
|
||||
|
||||
# map field values
|
||||
if df.fieldtype == "Table MultiSelect":
|
||||
move_table_multiselect_data(df)
|
||||
else:
|
||||
frappe.db.sql( # nosemgrep
|
||||
"""
|
||||
UPDATE `tabWebsite Item` wi, `tabItem` i
|
||||
SET wi.{0} = i.{0}
|
||||
WHERE wi.item_code = i.item_code
|
||||
""".format(
|
||||
row.fieldname
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,39 @@
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
|
||||
|
||||
def execute():
|
||||
accounting_dimensions = frappe.db.get_all(
|
||||
"Accounting Dimension", fields=["fieldname", "label", "document_type", "disabled"]
|
||||
)
|
||||
|
||||
if not accounting_dimensions:
|
||||
return
|
||||
|
||||
count = 1
|
||||
for d in accounting_dimensions:
|
||||
|
||||
if count % 2 == 0:
|
||||
insert_after_field = "dimension_col_break"
|
||||
else:
|
||||
insert_after_field = "accounting_dimensions_section"
|
||||
|
||||
for doctype in ["Purchase Order", "Purchase Receipt", "Sales Order"]:
|
||||
|
||||
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname})
|
||||
|
||||
if field:
|
||||
continue
|
||||
|
||||
df = {
|
||||
"fieldname": d.fieldname,
|
||||
"label": d.label,
|
||||
"fieldtype": "Link",
|
||||
"options": d.document_type,
|
||||
"insert_after": insert_after_field,
|
||||
}
|
||||
|
||||
create_custom_field(doctype, df, ignore_validate=False)
|
||||
frappe.clear_cache(doctype=doctype)
|
||||
|
||||
count += 1
|
||||
9
erpnext/patches/v13_0/datev_deprecation_warning.py
Normal file
9
erpnext/patches/v13_0/datev_deprecation_warning.py
Normal file
@@ -0,0 +1,9 @@
|
||||
import click
|
||||
|
||||
|
||||
def execute():
|
||||
click.secho(
|
||||
"DATEV reports are moved to a separate app and will be removed from ERPNext in version-14.\n"
|
||||
"Please install the app to continue using them: https://github.com/alyf-de/erpnext_datev",
|
||||
fg="yellow",
|
||||
)
|
||||
10
erpnext/patches/v13_0/education_deprecation_warning.py
Normal file
10
erpnext/patches/v13_0/education_deprecation_warning.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import click
|
||||
|
||||
|
||||
def execute():
|
||||
|
||||
click.secho(
|
||||
"Education Domain is moved to a separate app and will be removed from ERPNext in version-14.\n"
|
||||
"When upgrading to ERPNext version-14, please install the app to continue using the Education domain: https://github.com/frappe/education",
|
||||
fg="yellow",
|
||||
)
|
||||
@@ -10,54 +10,58 @@ def execute():
|
||||
|
||||
frappe.reload_doc("hr", "doctype", "Leave Encashment")
|
||||
|
||||
additional_salaries = frappe.get_all(
|
||||
"Additional Salary",
|
||||
fields=["name", "salary_slip", "type", "salary_component"],
|
||||
filters={"salary_slip": ["!=", ""]},
|
||||
group_by="salary_slip",
|
||||
)
|
||||
leave_encashments = frappe.get_all(
|
||||
"Leave Encashment",
|
||||
fields=["name", "additional_salary"],
|
||||
filters={"additional_salary": ["!=", ""]},
|
||||
)
|
||||
employee_incentives = frappe.get_all(
|
||||
"Employee Incentive",
|
||||
fields=["name", "additional_salary"],
|
||||
filters={"additional_salary": ["!=", ""]},
|
||||
)
|
||||
|
||||
for incentive in employee_incentives:
|
||||
frappe.db.sql(
|
||||
""" UPDATE `tabAdditional Salary`
|
||||
SET ref_doctype = 'Employee Incentive', ref_docname = %s
|
||||
WHERE name = %s
|
||||
""",
|
||||
(incentive["name"], incentive["additional_salary"]),
|
||||
if frappe.db.has_column("Leave Encashment", "additional_salary"):
|
||||
leave_encashments = frappe.get_all(
|
||||
"Leave Encashment",
|
||||
fields=["name", "additional_salary"],
|
||||
filters={"additional_salary": ["!=", ""]},
|
||||
)
|
||||
|
||||
for leave_encashment in leave_encashments:
|
||||
frappe.db.sql(
|
||||
""" UPDATE `tabAdditional Salary`
|
||||
SET ref_doctype = 'Leave Encashment', ref_docname = %s
|
||||
WHERE name = %s
|
||||
""",
|
||||
(leave_encashment["name"], leave_encashment["additional_salary"]),
|
||||
)
|
||||
|
||||
salary_slips = [sal["salary_slip"] for sal in additional_salaries]
|
||||
|
||||
for salary in additional_salaries:
|
||||
comp_type = "earnings" if salary["type"] == "Earning" else "deductions"
|
||||
if salary["salary_slip"] and salary_slips.count(salary["salary_slip"]) == 1:
|
||||
for leave_encashment in leave_encashments:
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE `tabSalary Detail`
|
||||
SET additional_salary = %s
|
||||
WHERE parenttype = 'Salary Slip'
|
||||
and parentfield = %s
|
||||
and parent = %s
|
||||
and salary_component = %s
|
||||
""" UPDATE `tabAdditional Salary`
|
||||
SET ref_doctype = 'Leave Encashment', ref_docname = %s
|
||||
WHERE name = %s
|
||||
""",
|
||||
(salary["name"], comp_type, salary["salary_slip"], salary["salary_component"]),
|
||||
(leave_encashment["name"], leave_encashment["additional_salary"]),
|
||||
)
|
||||
|
||||
if frappe.db.has_column("Employee Incentive", "additional_salary"):
|
||||
employee_incentives = frappe.get_all(
|
||||
"Employee Incentive",
|
||||
fields=["name", "additional_salary"],
|
||||
filters={"additional_salary": ["!=", ""]},
|
||||
)
|
||||
|
||||
for incentive in employee_incentives:
|
||||
frappe.db.sql(
|
||||
""" UPDATE `tabAdditional Salary`
|
||||
SET ref_doctype = 'Employee Incentive', ref_docname = %s
|
||||
WHERE name = %s
|
||||
""",
|
||||
(incentive["name"], incentive["additional_salary"]),
|
||||
)
|
||||
|
||||
if frappe.db.has_column("Additional Salary", "salary_slip"):
|
||||
additional_salaries = frappe.get_all(
|
||||
"Additional Salary",
|
||||
fields=["name", "salary_slip", "type", "salary_component"],
|
||||
filters={"salary_slip": ["!=", ""]},
|
||||
group_by="salary_slip",
|
||||
)
|
||||
|
||||
salary_slips = [sal["salary_slip"] for sal in additional_salaries]
|
||||
|
||||
for salary in additional_salaries:
|
||||
comp_type = "earnings" if salary["type"] == "Earning" else "deductions"
|
||||
if salary["salary_slip"] and salary_slips.count(salary["salary_slip"]) == 1:
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE `tabSalary Detail`
|
||||
SET additional_salary = %s
|
||||
WHERE parenttype = 'Salary Slip'
|
||||
and parentfield = %s
|
||||
and parent = %s
|
||||
and salary_component = %s
|
||||
""",
|
||||
(salary["name"], comp_type, salary["salary_slip"], salary["salary_component"]),
|
||||
)
|
||||
|
||||
@@ -376,13 +376,19 @@ class SalarySlip(TransactionBase):
|
||||
if joining_date and (getdate(self.start_date) < joining_date <= getdate(self.end_date)):
|
||||
start_date = joining_date
|
||||
unmarked_days = self.get_unmarked_days_based_on_doj_or_relieving(
|
||||
unmarked_days, include_holidays_in_total_working_days, self.start_date, joining_date
|
||||
unmarked_days,
|
||||
include_holidays_in_total_working_days,
|
||||
self.start_date,
|
||||
add_days(joining_date, -1),
|
||||
)
|
||||
|
||||
if relieving_date and (getdate(self.start_date) <= relieving_date < getdate(self.end_date)):
|
||||
end_date = relieving_date
|
||||
unmarked_days = self.get_unmarked_days_based_on_doj_or_relieving(
|
||||
unmarked_days, include_holidays_in_total_working_days, relieving_date, self.end_date
|
||||
unmarked_days,
|
||||
include_holidays_in_total_working_days,
|
||||
add_days(relieving_date, 1),
|
||||
self.end_date,
|
||||
)
|
||||
|
||||
# exclude days for which attendance has been marked
|
||||
@@ -408,10 +414,10 @@ class SalarySlip(TransactionBase):
|
||||
from erpnext.hr.doctype.employee.employee import is_holiday
|
||||
|
||||
if include_holidays_in_total_working_days:
|
||||
unmarked_days -= date_diff(end_date, start_date)
|
||||
unmarked_days -= date_diff(end_date, start_date) + 1
|
||||
else:
|
||||
# exclude only if not holidays
|
||||
for days in range(date_diff(end_date, start_date)):
|
||||
for days in range(date_diff(end_date, start_date) + 1):
|
||||
date = add_days(end_date, -days)
|
||||
if not is_holiday(self.employee, date):
|
||||
unmarked_days -= 1
|
||||
|
||||
@@ -128,6 +128,72 @@ class TestSalarySlip(unittest.TestCase):
|
||||
},
|
||||
)
|
||||
def test_payment_days_for_mid_joinee_including_holidays(self):
|
||||
no_of_days = self.get_no_of_days()
|
||||
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
||||
|
||||
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
|
||||
joining_date, relieving_date = add_days(month_start_date, 3), add_days(month_end_date, -5)
|
||||
|
||||
for days in range(date_diff(month_end_date, month_start_date) + 1):
|
||||
date = add_days(month_start_date, days)
|
||||
mark_attendance(new_emp_id, date, "Present", ignore_validate=True)
|
||||
|
||||
# Case 1: relieving in mid month
|
||||
frappe.db.set_value(
|
||||
"Employee",
|
||||
new_emp_id,
|
||||
{"date_of_joining": month_start_date, "relieving_date": relieving_date, "status": "Active"},
|
||||
)
|
||||
|
||||
new_ss = make_employee_salary_slip(
|
||||
"test_payment_days_based_on_joining_date@salary.com",
|
||||
"Monthly",
|
||||
"Test Payment Based On Attendence",
|
||||
)
|
||||
self.assertEqual(new_ss.payment_days, no_of_days[0] - 5)
|
||||
|
||||
# Case 2: joining in mid month
|
||||
frappe.db.set_value(
|
||||
"Employee",
|
||||
new_emp_id,
|
||||
{"date_of_joining": joining_date, "relieving_date": month_end_date, "status": "Active"},
|
||||
)
|
||||
|
||||
frappe.delete_doc("Salary Slip", new_ss.name, force=True)
|
||||
new_ss = make_employee_salary_slip(
|
||||
"test_payment_days_based_on_joining_date@salary.com",
|
||||
"Monthly",
|
||||
"Test Payment Based On Attendence",
|
||||
)
|
||||
self.assertEqual(new_ss.payment_days, no_of_days[0] - 3)
|
||||
|
||||
# Case 3: joining and relieving in mid-month
|
||||
frappe.db.set_value(
|
||||
"Employee",
|
||||
new_emp_id,
|
||||
{"date_of_joining": joining_date, "relieving_date": relieving_date, "status": "Left"},
|
||||
)
|
||||
|
||||
frappe.delete_doc("Salary Slip", new_ss.name, force=True)
|
||||
new_ss = make_employee_salary_slip(
|
||||
"test_payment_days_based_on_joining_date@salary.com",
|
||||
"Monthly",
|
||||
"Test Payment Based On Attendence",
|
||||
)
|
||||
|
||||
self.assertEqual(new_ss.total_working_days, no_of_days[0])
|
||||
self.assertEqual(new_ss.payment_days, no_of_days[0] - 8)
|
||||
|
||||
@change_settings(
|
||||
"Payroll Settings",
|
||||
{
|
||||
"payroll_based_on": "Attendance",
|
||||
"consider_unmarked_attendance_as": "Absent",
|
||||
"include_holidays_in_total_working_days": True,
|
||||
},
|
||||
)
|
||||
def test_payment_days_for_mid_joinee_including_holidays_and_unmarked_days(self):
|
||||
# tests mid month joining and relieving along with unmarked days
|
||||
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
|
||||
|
||||
no_of_days = self.get_no_of_days()
|
||||
@@ -135,12 +201,6 @@ class TestSalarySlip(unittest.TestCase):
|
||||
|
||||
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
|
||||
joining_date, relieving_date = add_days(month_start_date, 3), add_days(month_end_date, -5)
|
||||
frappe.db.set_value(
|
||||
"Employee",
|
||||
new_emp_id,
|
||||
{"date_of_joining": joining_date, "relieving_date": relieving_date, "status": "Left"},
|
||||
)
|
||||
|
||||
holidays = 0
|
||||
|
||||
for days in range(date_diff(relieving_date, joining_date) + 1):
|
||||
@@ -150,6 +210,12 @@ class TestSalarySlip(unittest.TestCase):
|
||||
else:
|
||||
holidays += 1
|
||||
|
||||
frappe.db.set_value(
|
||||
"Employee",
|
||||
new_emp_id,
|
||||
{"date_of_joining": joining_date, "relieving_date": relieving_date, "status": "Left"},
|
||||
)
|
||||
|
||||
new_ss = make_employee_salary_slip(
|
||||
"test_payment_days_based_on_joining_date@salary.com",
|
||||
"Monthly",
|
||||
|
||||
@@ -84,6 +84,7 @@ class Project(Document):
|
||||
type=task_details.type,
|
||||
issue=task_details.issue,
|
||||
is_group=task_details.is_group,
|
||||
color=task_details.color,
|
||||
)
|
||||
).insert()
|
||||
|
||||
|
||||
@@ -1500,6 +1500,11 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
return;
|
||||
}
|
||||
|
||||
// Target doc created from a mapped doc
|
||||
if (this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.frm.call({
|
||||
method: "erpnext.accounts.doctype.pricing_rule.pricing_rule.apply_pricing_rule",
|
||||
args: { args: args, doc: me.frm.doc },
|
||||
@@ -1616,7 +1621,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
me.remove_pricing_rule(frappe.get_doc(d.doctype, d.name));
|
||||
}
|
||||
|
||||
if (d.free_item_data) {
|
||||
if (d.free_item_data.length > 0) {
|
||||
me.apply_product_discount(d);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import traceback
|
||||
|
||||
import frappe
|
||||
import jwt
|
||||
import requests
|
||||
import six
|
||||
from frappe import _, bold
|
||||
from frappe.core.page.background_jobs.background_jobs import get_info
|
||||
@@ -57,6 +58,7 @@ def validate_eligibility(doc):
|
||||
invalid_company = not frappe.db.get_value("E Invoice User", {"company": doc.get("company")})
|
||||
invalid_supply_type = doc.get("gst_category") not in [
|
||||
"Registered Regular",
|
||||
"Registered Composition",
|
||||
"SEZ",
|
||||
"Overseas",
|
||||
"Deemed Export",
|
||||
@@ -124,24 +126,33 @@ def read_json(name):
|
||||
|
||||
def get_transaction_details(invoice):
|
||||
supply_type = ""
|
||||
if invoice.gst_category == "Registered Regular":
|
||||
if (
|
||||
invoice.gst_category == "Registered Regular" or invoice.gst_category == "Registered Composition"
|
||||
):
|
||||
supply_type = "B2B"
|
||||
elif invoice.gst_category == "SEZ":
|
||||
supply_type = "SEZWOP"
|
||||
if invoice.export_type == "Without Payment of Tax":
|
||||
supply_type = "SEZWOP"
|
||||
else:
|
||||
supply_type = "SEZWP"
|
||||
elif invoice.gst_category == "Overseas":
|
||||
supply_type = "EXPWOP"
|
||||
if invoice.export_type == "Without Payment of Tax":
|
||||
supply_type = "EXPWOP"
|
||||
else:
|
||||
supply_type = "EXPWP"
|
||||
elif invoice.gst_category == "Deemed Export":
|
||||
supply_type = "DEXP"
|
||||
|
||||
if not supply_type:
|
||||
rr, sez, overseas, export = (
|
||||
rr, rc, sez, overseas, export = (
|
||||
bold("Registered Regular"),
|
||||
bold("Registered Composition"),
|
||||
bold("SEZ"),
|
||||
bold("Overseas"),
|
||||
bold("Deemed Export"),
|
||||
)
|
||||
frappe.throw(
|
||||
_("GST category should be one of {}, {}, {}, {}").format(rr, sez, overseas, export),
|
||||
_("GST category should be one of {}, {}, {}, {}, {}").format(rr, rc, sez, overseas, export),
|
||||
title=_("Invalid Supply Type"),
|
||||
)
|
||||
|
||||
@@ -315,10 +326,14 @@ def update_item_taxes(invoice, item):
|
||||
item.cess_rate += item_tax_rate
|
||||
item.cess_amount += abs(item_tax_amount_after_discount)
|
||||
|
||||
for tax_type in ["igst", "cgst", "sgst"]:
|
||||
for tax_type in ["igst", "cgst", "sgst", "utgst"]:
|
||||
if t.account_head in gst_accounts[f"{tax_type}_account"]:
|
||||
item.tax_rate += item_tax_rate
|
||||
item[f"{tax_type}_amount"] += abs(item_tax_amount)
|
||||
if tax_type == "utgst":
|
||||
# utgst taxes are reported same as sgst tax
|
||||
item["sgst_amount"] += abs(item_tax_amount)
|
||||
else:
|
||||
item[f"{tax_type}_amount"] += abs(item_tax_amount)
|
||||
else:
|
||||
# TODO: other charges per item
|
||||
pass
|
||||
@@ -360,11 +375,15 @@ def update_invoice_taxes(invoice, invoice_value_details):
|
||||
# using after discount amt since item also uses after discount amt for cess calc
|
||||
invoice_value_details.total_cess_amt += abs(t.base_tax_amount_after_discount_amount)
|
||||
|
||||
for tax_type in ["igst", "cgst", "sgst"]:
|
||||
for tax_type in ["igst", "cgst", "sgst", "utgst"]:
|
||||
if t.account_head in gst_accounts[f"{tax_type}_account"]:
|
||||
if tax_type == "utgst":
|
||||
invoice_value_details["total_sgst_amt"] += abs(tax_amount)
|
||||
else:
|
||||
invoice_value_details[f"total_{tax_type}_amt"] += abs(tax_amount)
|
||||
|
||||
invoice_value_details[f"total_{tax_type}_amt"] += abs(tax_amount)
|
||||
update_other_charges(t, invoice_value_details, gst_accounts_list, invoice, considered_rows)
|
||||
|
||||
else:
|
||||
invoice_value_details.total_other_charges += abs(tax_amount)
|
||||
|
||||
@@ -544,6 +563,7 @@ def validate_totals(einvoice):
|
||||
+ flt(value_details["CgstVal"])
|
||||
+ flt(value_details["SgstVal"])
|
||||
+ flt(value_details["IgstVal"])
|
||||
+ flt(value_details["CesVal"])
|
||||
+ flt(value_details["OthChrg"])
|
||||
- flt(value_details["Discount"])
|
||||
)
|
||||
@@ -820,14 +840,25 @@ class GSPConnector:
|
||||
return self.e_invoice_settings.auth_token
|
||||
|
||||
def make_request(self, request_type, url, headers=None, data=None):
|
||||
if request_type == "post":
|
||||
res = make_post_request(url, headers=headers, data=data)
|
||||
else:
|
||||
res = make_get_request(url, headers=headers, data=data)
|
||||
try:
|
||||
if request_type == "post":
|
||||
res = make_post_request(url, headers=headers, data=data)
|
||||
else:
|
||||
res = make_get_request(url, headers=headers, data=data)
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code in [401, 403] and not hasattr(self, "token_auto_refreshed"):
|
||||
self.auto_refresh_token()
|
||||
headers = self.get_headers()
|
||||
return self.make_request(request_type, url, headers, data)
|
||||
|
||||
self.log_request(url, headers, data, res)
|
||||
return res
|
||||
|
||||
def auto_refresh_token(self):
|
||||
self.fetch_auth_token()
|
||||
self.token_auto_refreshed = True
|
||||
|
||||
def log_request(self, url, headers, data, res):
|
||||
headers.update({"password": self.credentials.password})
|
||||
request_log = frappe.get_doc(
|
||||
@@ -1065,7 +1096,7 @@ class GSPConnector:
|
||||
"Distance": cint(eway_bill_details.distance),
|
||||
"TransMode": eway_bill_details.mode_of_transport,
|
||||
"TransId": eway_bill_details.gstin,
|
||||
"TransName": eway_bill_details.transporter,
|
||||
"TransName": eway_bill_details.name,
|
||||
"TrnDocDt": eway_bill_details.document_date,
|
||||
"TrnDocNo": eway_bill_details.document_name,
|
||||
"VehNo": eway_bill_details.vehicle_no,
|
||||
|
||||
@@ -714,7 +714,7 @@ def get_custom_fields():
|
||||
insert_after="customer",
|
||||
no_copy=1,
|
||||
print_hide=1,
|
||||
depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0',
|
||||
depends_on='eval:in_list(["Registered Regular", "Registered Composition", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0',
|
||||
),
|
||||
dict(
|
||||
fieldname="irn_cancelled",
|
||||
|
||||
@@ -341,7 +341,7 @@ def get_tax_template(master_doctype, company, is_inter_state, state_code):
|
||||
tax_categories = frappe.get_all(
|
||||
"Tax Category",
|
||||
fields=["name", "is_inter_state", "gst_state"],
|
||||
filters={"is_inter_state": is_inter_state, "is_reverse_charge": 0},
|
||||
filters={"is_inter_state": is_inter_state, "is_reverse_charge": 0, "disabled": 0},
|
||||
)
|
||||
|
||||
default_tax = ""
|
||||
@@ -825,7 +825,7 @@ def get_gst_accounts(
|
||||
gst_settings_accounts = frappe.get_all(
|
||||
"GST Account",
|
||||
filters=filters,
|
||||
fields=["cgst_account", "sgst_account", "igst_account", "cess_account"],
|
||||
fields=["cgst_account", "sgst_account", "igst_account", "cess_account", "utgst_account"],
|
||||
)
|
||||
|
||||
if not gst_settings_accounts and not frappe.flags.in_test and not frappe.flags.in_migrate:
|
||||
|
||||
@@ -78,8 +78,9 @@ frappe.query_reports["GSTR-1"] = {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
report.page.add_inner_button(__("Download as JSON"), function () {
|
||||
let filters = report.get_values();
|
||||
|
||||
frappe.call({
|
||||
method: 'erpnext.regional.report.gstr_1.gstr_1.get_json',
|
||||
args: {
|
||||
|
||||
@@ -95,10 +95,9 @@ class VATAuditReport(object):
|
||||
as_dict=1,
|
||||
)
|
||||
for d in items:
|
||||
if d.item_code not in self.invoice_items.get(d.parent, {}):
|
||||
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, {"net_amount": 0.0})
|
||||
self.invoice_items[d.parent][d.item_code]["net_amount"] += d.get("base_net_amount", 0)
|
||||
self.invoice_items[d.parent][d.item_code]["is_zero_rated"] = d.is_zero_rated
|
||||
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, {"net_amount": 0.0})
|
||||
self.invoice_items[d.parent][d.item_code]["net_amount"] += d.get("base_net_amount", 0)
|
||||
self.invoice_items[d.parent][d.item_code]["is_zero_rated"] = d.is_zero_rated
|
||||
|
||||
def get_items_based_on_tax_rate(self, doctype):
|
||||
self.items_based_on_tax_rate = frappe._dict()
|
||||
@@ -110,7 +109,7 @@ class VATAuditReport(object):
|
||||
self.tax_details = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount
|
||||
parent, account_head, item_wise_tax_detail
|
||||
FROM
|
||||
`tab%s`
|
||||
WHERE
|
||||
@@ -123,7 +122,7 @@ class VATAuditReport(object):
|
||||
tuple([doctype] + list(self.invoices.keys())),
|
||||
)
|
||||
|
||||
for parent, account, item_wise_tax_detail, tax_amount in self.tax_details:
|
||||
for parent, account, item_wise_tax_detail in self.tax_details:
|
||||
if item_wise_tax_detail:
|
||||
try:
|
||||
if account in self.sa_vat_accounts:
|
||||
@@ -135,7 +134,7 @@ class VATAuditReport(object):
|
||||
# to skip items with non-zero tax rate in multiple rows
|
||||
if taxes[0] == 0 and not is_zero_rated:
|
||||
continue
|
||||
tax_rate, item_amount_map = self.get_item_amount_map(parent, item_code, taxes)
|
||||
tax_rate = self.get_item_amount_map(parent, item_code, taxes)
|
||||
|
||||
if tax_rate is not None:
|
||||
rate_based_dict = self.items_based_on_tax_rate.setdefault(parent, {}).setdefault(
|
||||
@@ -151,16 +150,22 @@ class VATAuditReport(object):
|
||||
tax_rate = taxes[0]
|
||||
tax_amount = taxes[1]
|
||||
gross_amount = net_amount + tax_amount
|
||||
item_amount_map = self.item_tax_rate.setdefault(parent, {}).setdefault(item_code, [])
|
||||
amount_dict = {
|
||||
"tax_rate": tax_rate,
|
||||
"gross_amount": gross_amount,
|
||||
"tax_amount": tax_amount,
|
||||
"net_amount": net_amount,
|
||||
}
|
||||
item_amount_map.append(amount_dict)
|
||||
|
||||
return tax_rate, item_amount_map
|
||||
self.item_tax_rate.setdefault(parent, {}).setdefault(
|
||||
item_code,
|
||||
{
|
||||
"tax_rate": tax_rate,
|
||||
"gross_amount": 0.0,
|
||||
"tax_amount": 0.0,
|
||||
"net_amount": 0.0,
|
||||
},
|
||||
)
|
||||
|
||||
self.item_tax_rate[parent][item_code]["net_amount"] += net_amount
|
||||
self.item_tax_rate[parent][item_code]["tax_amount"] += tax_amount
|
||||
self.item_tax_rate[parent][item_code]["gross_amount"] += gross_amount
|
||||
|
||||
return tax_rate
|
||||
|
||||
def get_conditions(self):
|
||||
conditions = ""
|
||||
@@ -205,9 +210,10 @@ class VATAuditReport(object):
|
||||
for inv, inv_data in self.invoices.items():
|
||||
if self.items_based_on_tax_rate.get(inv):
|
||||
for rate, items in self.items_based_on_tax_rate.get(inv).items():
|
||||
row = {"tax_amount": 0.0, "gross_amount": 0.0, "net_amount": 0.0}
|
||||
|
||||
consolidated_data_map.setdefault(rate, {"data": []})
|
||||
for item in items:
|
||||
row = {}
|
||||
item_details = self.item_tax_rate.get(inv).get(item)
|
||||
row["account"] = inv_data.get("account")
|
||||
row["posting_date"] = formatdate(inv_data.get("posting_date"), "dd-mm-yyyy")
|
||||
@@ -216,10 +222,11 @@ class VATAuditReport(object):
|
||||
row["party_type"] = "Customer" if doctype == "Sales Invoice" else "Supplier"
|
||||
row["party"] = inv_data.get("party")
|
||||
row["remarks"] = inv_data.get("remarks")
|
||||
row["gross_amount"] = item_details[0].get("gross_amount")
|
||||
row["tax_amount"] = item_details[0].get("tax_amount")
|
||||
row["net_amount"] = item_details[0].get("net_amount")
|
||||
consolidated_data_map[rate]["data"].append(row)
|
||||
row["gross_amount"] += item_details.get("gross_amount")
|
||||
row["tax_amount"] += item_details.get("tax_amount")
|
||||
row["net_amount"] += item_details.get("net_amount")
|
||||
|
||||
consolidated_data_map[rate]["data"].append(row)
|
||||
|
||||
return consolidated_data_map
|
||||
|
||||
|
||||
@@ -245,7 +245,7 @@ def make_custom_fields():
|
||||
"Supplier Quotation Item": invoice_item_fields,
|
||||
}
|
||||
|
||||
create_custom_fields(custom_fields)
|
||||
create_custom_fields(custom_fields, ignore_validate=True)
|
||||
|
||||
|
||||
def add_print_formats():
|
||||
|
||||
@@ -100,7 +100,8 @@ class Customer(TransactionBase):
|
||||
@frappe.whitelist()
|
||||
def get_customer_group_details(self):
|
||||
doc = frappe.get_doc("Customer Group", self.customer_group)
|
||||
self.accounts = self.credit_limits = []
|
||||
self.accounts = []
|
||||
self.credit_limits = []
|
||||
self.payment_terms = self.default_price_list = ""
|
||||
|
||||
tables = [["accounts", "account"], ["credit_limits", "credit_limit"]]
|
||||
|
||||
@@ -47,7 +47,8 @@ class TestCustomer(FrappeTestCase):
|
||||
c_doc.customer_name = "Testing Customer"
|
||||
c_doc.customer_group = "_Testing Customer Group"
|
||||
c_doc.payment_terms = c_doc.default_price_list = ""
|
||||
c_doc.accounts = c_doc.credit_limits = []
|
||||
c_doc.accounts = []
|
||||
c_doc.credit_limits = []
|
||||
c_doc.insert()
|
||||
c_doc.get_customer_group_details()
|
||||
self.assertEqual(c_doc.payment_terms, "_Test Payment Term Template 3")
|
||||
|
||||
@@ -26,6 +26,7 @@ class Quotation(SellingController):
|
||||
self.set_status()
|
||||
self.validate_uom_is_integer("stock_uom", "qty")
|
||||
self.validate_valid_till()
|
||||
self.validate_shopping_cart_items()
|
||||
self.set_customer_name()
|
||||
if self.items:
|
||||
self.with_items = 1
|
||||
@@ -38,6 +39,26 @@ class Quotation(SellingController):
|
||||
if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date):
|
||||
frappe.throw(_("Valid till date cannot be before transaction date"))
|
||||
|
||||
def validate_shopping_cart_items(self):
|
||||
if self.order_type != "Shopping Cart":
|
||||
return
|
||||
|
||||
for item in self.items:
|
||||
has_web_item = frappe.db.exists("Website Item", {"item_code": item.item_code})
|
||||
|
||||
# If variant is unpublished but template is published: valid
|
||||
template = frappe.get_cached_value("Item", item.item_code, "variant_of")
|
||||
if template and not has_web_item:
|
||||
has_web_item = frappe.db.exists("Website Item", {"item_code": template})
|
||||
|
||||
if not has_web_item:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Item {1} must have a Website Item for Shopping Cart Quotations").format(
|
||||
item.idx, frappe.bold(item.item_code)
|
||||
),
|
||||
title=_("Unpublished Item"),
|
||||
)
|
||||
|
||||
def has_sales_order(self):
|
||||
return frappe.db.get_value("Sales Order Item", {"prevdoc_docname": self.name, "docstatus": 1})
|
||||
|
||||
|
||||
@@ -130,6 +130,15 @@ class TestQuotation(FrappeTestCase):
|
||||
quotation.submit()
|
||||
self.assertRaises(frappe.ValidationError, make_sales_order, quotation.name)
|
||||
|
||||
def test_shopping_cart_without_website_item(self):
|
||||
if frappe.db.exists("Website Item", {"item_code": "_Test Item Home Desktop 100"}):
|
||||
frappe.get_last_doc("Website Item", {"item_code": "_Test Item Home Desktop 100"}).delete()
|
||||
|
||||
quotation = frappe.copy_doc(test_records[0])
|
||||
quotation.order_type = "Shopping Cart"
|
||||
quotation.valid_till = getdate()
|
||||
self.assertRaises(frappe.ValidationError, quotation.validate)
|
||||
|
||||
def test_create_quotation_with_margin(self):
|
||||
from erpnext.selling.doctype.quotation.quotation import make_sales_order
|
||||
from erpnext.selling.doctype.sales_order.sales_order import (
|
||||
|
||||
@@ -152,7 +152,9 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
||||
}
|
||||
}
|
||||
|
||||
this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create'));
|
||||
if (flt(doc.per_picked, 6) < 100 && flt(doc.per_delivered, 6) < 100) {
|
||||
this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create'));
|
||||
}
|
||||
|
||||
const order_is_a_sale = ["Sales", "Shopping Cart"].indexOf(doc.order_type) !== -1;
|
||||
const order_is_maintenance = ["Maintenance"].indexOf(doc.order_type) !== -1;
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
"po_no",
|
||||
"po_date",
|
||||
"tax_id",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"contact_info",
|
||||
"customer_address",
|
||||
"address_display",
|
||||
@@ -113,7 +117,6 @@
|
||||
"is_internal_customer",
|
||||
"represents_company",
|
||||
"inter_company_order_reference",
|
||||
"project",
|
||||
"party_account_currency",
|
||||
"column_break_77",
|
||||
"source",
|
||||
@@ -1520,14 +1523,31 @@
|
||||
"fieldname": "per_picked",
|
||||
"fieldtype": "Percent",
|
||||
"label": "% Picked",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-03-15 21:38:31.437586",
|
||||
"modified": "2022-04-26 14:38:18.350207",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order",
|
||||
@@ -1606,4 +1626,4 @@
|
||||
"title_field": "customer_name",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,6 +455,16 @@ class SalesOrder(SellingController):
|
||||
if tot_qty != 0:
|
||||
self.db_set("per_delivered", flt(delivered_qty / tot_qty) * 100, update_modified=False)
|
||||
|
||||
def update_picking_status(self):
|
||||
total_picked_qty = 0.0
|
||||
total_qty = 0.0
|
||||
for so_item in self.items:
|
||||
total_picked_qty += flt(so_item.picked_qty)
|
||||
total_qty += flt(so_item.stock_qty)
|
||||
per_picked = total_picked_qty / total_qty * 100
|
||||
|
||||
self.db_set("per_picked", flt(per_picked), update_modified=False)
|
||||
|
||||
def set_indicator(self):
|
||||
"""Set indicator for portal"""
|
||||
if self.per_billed < 100 and self.per_delivered < 100:
|
||||
@@ -1302,9 +1312,30 @@ def make_inter_company_purchase_order(source_name, target_doc=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_pick_list(source_name, target_doc=None):
|
||||
def update_item_quantity(source, target, source_parent):
|
||||
target.qty = flt(source.qty) - flt(source.delivered_qty)
|
||||
target.stock_qty = (flt(source.qty) - flt(source.delivered_qty)) * flt(source.conversion_factor)
|
||||
from erpnext.stock.doctype.packed_item.packed_item import is_product_bundle
|
||||
|
||||
def update_item_quantity(source, target, source_parent) -> None:
|
||||
picked_qty = flt(source.picked_qty) / (flt(source.conversion_factor) or 1)
|
||||
qty_to_be_picked = flt(source.qty) - max(picked_qty, flt(source.delivered_qty))
|
||||
|
||||
target.qty = qty_to_be_picked
|
||||
target.stock_qty = qty_to_be_picked * flt(source.conversion_factor)
|
||||
|
||||
def update_packed_item_qty(source, target, source_parent) -> None:
|
||||
qty = flt(source.qty)
|
||||
for item in source_parent.items:
|
||||
if source.parent_detail_docname == item.name:
|
||||
picked_qty = flt(item.picked_qty) / (flt(item.conversion_factor) or 1)
|
||||
pending_percent = (item.qty - max(picked_qty, item.delivered_qty)) / item.qty
|
||||
target.qty = target.stock_qty = qty * pending_percent
|
||||
return
|
||||
|
||||
def should_pick_order_item(item) -> bool:
|
||||
return (
|
||||
abs(item.delivered_qty) < abs(item.qty)
|
||||
and item.delivered_by_supplier != 1
|
||||
and not is_product_bundle(item.item_code)
|
||||
)
|
||||
|
||||
doc = get_mapped_doc(
|
||||
"Sales Order",
|
||||
@@ -1315,8 +1346,17 @@ def create_pick_list(source_name, target_doc=None):
|
||||
"doctype": "Pick List Item",
|
||||
"field_map": {"parent": "sales_order", "name": "sales_order_item"},
|
||||
"postprocess": update_item_quantity,
|
||||
"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty)
|
||||
and doc.delivered_by_supplier != 1,
|
||||
"condition": should_pick_order_item,
|
||||
},
|
||||
"Packed Item": {
|
||||
"doctype": "Pick List Item",
|
||||
"field_map": {
|
||||
"parent": "sales_order",
|
||||
"name": "sales_order_item",
|
||||
"parent_detail_docname": "product_bundle_item",
|
||||
},
|
||||
"field_no_map": ["picked_qty"],
|
||||
"postprocess": update_packed_item_qty,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
|
||||
@@ -801,13 +801,15 @@
|
||||
{
|
||||
"fieldname": "picked_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Picked Qty"
|
||||
"label": "Picked Qty (in Stock UOM)",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-03-15 20:17:33.984799",
|
||||
"modified": "2022-04-27 03:15:34.366563",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order Item",
|
||||
|
||||
@@ -343,9 +343,9 @@ erpnext.PointOfSale.Controller = class {
|
||||
toggle_other_sections: (show) => {
|
||||
if (show) {
|
||||
this.item_details.$component.is(':visible') ? this.item_details.$component.css('display', 'none') : '';
|
||||
this.item_selector.$component.css('display', 'none');
|
||||
this.item_selector.toggle_component(false);
|
||||
} else {
|
||||
this.item_selector.$component.css('display', 'flex');
|
||||
this.item_selector.toggle_component(true);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -130,10 +130,10 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
},
|
||||
cols: 5,
|
||||
keys: [
|
||||
[ 1, 2, 3, __('Quantity') ],
|
||||
[ 4, 5, 6, __('Discount') ],
|
||||
[ 7, 8, 9, __('Rate') ],
|
||||
[ '.', 0, __('Delete'), __('Remove') ]
|
||||
[ 1, 2, 3, 'Quantity' ],
|
||||
[ 4, 5, 6, 'Discount' ],
|
||||
[ 7, 8, 9, 'Rate' ],
|
||||
[ '.', 0, 'Delete', 'Remove' ]
|
||||
],
|
||||
css_classes: [
|
||||
[ '', '', '', 'col-span-2' ],
|
||||
|
||||
@@ -179,6 +179,25 @@ erpnext.PointOfSale.ItemSelector = class {
|
||||
});
|
||||
this.search_field.toggle_label(false);
|
||||
this.item_group_field.toggle_label(false);
|
||||
|
||||
this.attach_clear_btn();
|
||||
}
|
||||
|
||||
attach_clear_btn() {
|
||||
this.search_field.$wrapper.find('.control-input').append(
|
||||
`<span class="link-btn" style="top: 2px;">
|
||||
<a class="btn-open no-decoration" title="${__("Clear")}">
|
||||
${frappe.utils.icon('close', 'sm')}
|
||||
</a>
|
||||
</span>`
|
||||
);
|
||||
|
||||
this.$clear_search_btn = this.search_field.$wrapper.find('.link-btn');
|
||||
|
||||
this.$clear_search_btn.on('click', 'a', () => {
|
||||
this.set_search_value('');
|
||||
this.search_field.set_focus();
|
||||
});
|
||||
}
|
||||
|
||||
set_search_value(value) {
|
||||
@@ -252,6 +271,16 @@ erpnext.PointOfSale.ItemSelector = class {
|
||||
const search_term = e.target.value;
|
||||
this.filter_items({ search_term });
|
||||
}, 300);
|
||||
|
||||
this.$clear_search_btn.toggle(
|
||||
Boolean(this.search_field.$input.val())
|
||||
);
|
||||
});
|
||||
|
||||
this.search_field.$input.on('focus', () => {
|
||||
this.$clear_search_btn.toggle(
|
||||
Boolean(this.search_field.$input.val())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -284,7 +313,7 @@ erpnext.PointOfSale.ItemSelector = class {
|
||||
if (this.items.length == 1) {
|
||||
this.$items_container.find(".item-wrapper").click();
|
||||
frappe.utils.play_sound("submit");
|
||||
$(this.search_field.$input[0]).val("").trigger("input");
|
||||
this.set_search_value('');
|
||||
} else if (this.items.length == 0 && this.barcode_scanned) {
|
||||
// only show alert of barcode is scanned and enter is pressed
|
||||
frappe.show_alert({
|
||||
@@ -293,7 +322,7 @@ erpnext.PointOfSale.ItemSelector = class {
|
||||
});
|
||||
frappe.utils.play_sound("error");
|
||||
this.barcode_scanned = false;
|
||||
$(this.search_field.$input[0]).val("").trigger("input");
|
||||
this.set_search_value('');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -350,6 +379,7 @@ erpnext.PointOfSale.ItemSelector = class {
|
||||
}
|
||||
|
||||
toggle_component(show) {
|
||||
show ? this.$component.css('display', 'flex') : this.$component.css('display', 'none');
|
||||
this.set_search_value('');
|
||||
this.$component.css('display', show ? 'flex': 'none');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ erpnext.PointOfSale.NumberPad = class {
|
||||
const fieldname = fieldnames && fieldnames[number] ?
|
||||
fieldnames[number] : typeof number === 'string' ? frappe.scrub(number) : number;
|
||||
|
||||
return a2 + `<div class="numpad-btn ${class_to_append}" data-button-value="${fieldname}">${number}</div>`;
|
||||
return a2 + `<div class="numpad-btn ${class_to_append}" data-button-value="${fieldname}">${__(number)}</div>`;
|
||||
}, '');
|
||||
}, '');
|
||||
}
|
||||
|
||||
@@ -27,28 +27,55 @@ function get_filters() {
|
||||
"default": frappe.datetime.get_today()
|
||||
},
|
||||
{
|
||||
"fieldname":"sales_order",
|
||||
"label": __("Sales Order"),
|
||||
"fieldtype": "MultiSelectList",
|
||||
"fieldname":"customer_group",
|
||||
"label": __("Customer Group"),
|
||||
"fieldtype": "Link",
|
||||
"width": 100,
|
||||
"options": "Sales Order",
|
||||
"get_data": function(txt) {
|
||||
return frappe.db.get_link_options("Sales Order", txt, this.filters());
|
||||
},
|
||||
"filters": () => {
|
||||
return {
|
||||
docstatus: 1,
|
||||
payment_terms_template: ['not in', ['']],
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
transaction_date: ['between', [frappe.query_report.get_filter_value("period_start_date"), frappe.query_report.get_filter_value("period_end_date")]]
|
||||
"options": "Customer Group",
|
||||
},
|
||||
{
|
||||
"fieldname":"customer",
|
||||
"label": __("Customer"),
|
||||
"fieldtype": "Link",
|
||||
"width": 100,
|
||||
"options": "Customer",
|
||||
"get_query": () => {
|
||||
var customer_group = frappe.query_report.get_filter_value('customer_group');
|
||||
return{
|
||||
"query": "erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order.get_customers_or_items",
|
||||
"filters": [
|
||||
['Customer', 'disabled', '=', '0'],
|
||||
['Customer Group','name', '=', customer_group]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"item_group",
|
||||
"label": __("Item Group"),
|
||||
"fieldtype": "Link",
|
||||
"width": 100,
|
||||
"options": "Item Group",
|
||||
|
||||
},
|
||||
{
|
||||
"fieldname":"item",
|
||||
"label": __("Item"),
|
||||
"fieldtype": "Link",
|
||||
"width": 100,
|
||||
"options": "Item",
|
||||
"get_query": () => {
|
||||
var item_group = frappe.query_report.get_filter_value('item_group');
|
||||
return{
|
||||
"query": "erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order.get_customers_or_items",
|
||||
"filters": [
|
||||
['Item', 'disabled', '=', '0'],
|
||||
['Item Group','name', '=', item_group]
|
||||
]
|
||||
}
|
||||
},
|
||||
on_change: function(){
|
||||
frappe.query_report.refresh();
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb, query_builder
|
||||
from frappe.query_builder import functions
|
||||
from frappe.query_builder import Criterion, functions
|
||||
|
||||
|
||||
def get_columns():
|
||||
@@ -14,6 +14,12 @@ def get_columns():
|
||||
"fieldtype": "Link",
|
||||
"options": "Sales Order",
|
||||
},
|
||||
{
|
||||
"label": _("Customer"),
|
||||
"fieldname": "customer",
|
||||
"fieldtype": "Link",
|
||||
"options": "Customer",
|
||||
},
|
||||
{
|
||||
"label": _("Posting Date"),
|
||||
"fieldname": "submitted",
|
||||
@@ -67,6 +73,55 @@ def get_columns():
|
||||
return columns
|
||||
|
||||
|
||||
def get_descendants_of(doctype, group_name):
|
||||
group_doc = qb.DocType(doctype)
|
||||
# get lft and rgt of group node
|
||||
lft, rgt = (
|
||||
qb.from_(group_doc).select(group_doc.lft, group_doc.rgt).where(group_doc.name == group_name)
|
||||
).run()[0]
|
||||
|
||||
# get all children of group node
|
||||
query = (
|
||||
qb.from_(group_doc).select(group_doc.name).where((group_doc.lft >= lft) & (group_doc.rgt <= rgt))
|
||||
)
|
||||
|
||||
child_nodes = []
|
||||
for x in query.run():
|
||||
child_nodes.append(x[0])
|
||||
|
||||
return child_nodes
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_customers_or_items(doctype, txt, searchfield, start, page_len, filters):
|
||||
filter_list = []
|
||||
if isinstance(filters, list):
|
||||
for item in filters:
|
||||
if item[0] == doctype:
|
||||
filter_list.append(item)
|
||||
elif item[0] == "Customer Group":
|
||||
if item[3] != "":
|
||||
filter_list.append(
|
||||
[doctype, "customer_group", "in", get_descendants_of("Customer Group", item[3])]
|
||||
)
|
||||
elif item[0] == "Item Group":
|
||||
if item[3] != "":
|
||||
filter_list.append([doctype, "item_group", "in", get_descendants_of("Item Group", item[3])])
|
||||
|
||||
if searchfield and txt:
|
||||
filter_list.append([doctype, searchfield, "like", "%%%s%%" % txt])
|
||||
|
||||
return frappe.desk.reportview.execute(
|
||||
doctype,
|
||||
filters=filter_list,
|
||||
fields=["name", "customer_group"] if doctype == "Customer" else ["name", "item_group"],
|
||||
limit_start=start,
|
||||
limit_page_length=page_len,
|
||||
as_list=True,
|
||||
)
|
||||
|
||||
|
||||
def get_conditions(filters):
|
||||
"""
|
||||
Convert filter options to conditions used in query
|
||||
@@ -79,11 +134,37 @@ def get_conditions(filters):
|
||||
conditions.start_date = filters.period_start_date or frappe.utils.add_months(
|
||||
conditions.end_date, -1
|
||||
)
|
||||
conditions.sales_order = filters.sales_order or []
|
||||
|
||||
return conditions
|
||||
|
||||
|
||||
def build_filter_criterions(filters):
|
||||
filters = frappe._dict(filters) if filters else frappe._dict({})
|
||||
qb_criterions = []
|
||||
|
||||
if filters.customer_group:
|
||||
qb_criterions.append(
|
||||
qb.DocType("Sales Order").customer_group.isin(
|
||||
get_descendants_of("Customer Group", filters.customer_group)
|
||||
)
|
||||
)
|
||||
|
||||
if filters.customer:
|
||||
qb_criterions.append(qb.DocType("Sales Order").customer == filters.customer)
|
||||
|
||||
if filters.item_group:
|
||||
qb_criterions.append(
|
||||
qb.DocType("Sales Order Item").item_group.isin(
|
||||
get_descendants_of("Item Group", filters.item_group)
|
||||
)
|
||||
)
|
||||
|
||||
if filters.item:
|
||||
qb_criterions.append(qb.DocType("Sales Order Item").item_code == filters.item)
|
||||
|
||||
return qb_criterions
|
||||
|
||||
|
||||
def get_so_with_invoices(filters):
|
||||
"""
|
||||
Get Sales Order with payment terms template with their associated Invoices
|
||||
@@ -92,16 +173,23 @@ def get_so_with_invoices(filters):
|
||||
|
||||
so = qb.DocType("Sales Order")
|
||||
ps = qb.DocType("Payment Schedule")
|
||||
soi = qb.DocType("Sales Order Item")
|
||||
|
||||
conditions = get_conditions(filters)
|
||||
filter_criterions = build_filter_criterions(filters)
|
||||
|
||||
datediff = query_builder.CustomFunction("DATEDIFF", ["cur_date", "due_date"])
|
||||
ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"])
|
||||
|
||||
conditions = get_conditions(filters)
|
||||
query_so = (
|
||||
qb.from_(so)
|
||||
.join(soi)
|
||||
.on(soi.parent == so.name)
|
||||
.join(ps)
|
||||
.on(ps.parent == so.name)
|
||||
.select(
|
||||
so.name,
|
||||
so.customer,
|
||||
so.transaction_date.as_("submitted"),
|
||||
ifelse(datediff(ps.due_date, functions.CurDate()) < 0, "Overdue", "Unpaid").as_("status"),
|
||||
ps.payment_term,
|
||||
@@ -117,12 +205,10 @@ def get_so_with_invoices(filters):
|
||||
& (so.company == conditions.company)
|
||||
& (so.transaction_date[conditions.start_date : conditions.end_date])
|
||||
)
|
||||
.where(Criterion.all(filter_criterions))
|
||||
.orderby(so.name, so.transaction_date, ps.due_date)
|
||||
)
|
||||
|
||||
if conditions.sales_order != []:
|
||||
query_so = query_so.where(so.name.isin(conditions.sales_order))
|
||||
|
||||
sorders = query_so.run(as_dict=True)
|
||||
|
||||
invoices = []
|
||||
|
||||
@@ -11,10 +11,13 @@ from erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_s
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Template"]
|
||||
test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Template", "Customer"]
|
||||
|
||||
|
||||
class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_payment_terms_template(self):
|
||||
# create template for 50-50 payments
|
||||
template = None
|
||||
@@ -48,9 +51,9 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
|
||||
template.insert()
|
||||
self.template = template
|
||||
|
||||
def test_payment_terms_status(self):
|
||||
def test_01_payment_terms_status(self):
|
||||
self.create_payment_terms_template()
|
||||
item = create_item(item_code="_Test Excavator", is_stock_item=0)
|
||||
item = create_item(item_code="_Test Excavator 1", is_stock_item=0)
|
||||
so = make_sales_order(
|
||||
transaction_date="2021-06-15",
|
||||
delivery_date=add_days("2021-06-15", -30),
|
||||
@@ -78,13 +81,14 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
|
||||
"company": "_Test Company",
|
||||
"period_start_date": "2021-06-01",
|
||||
"period_end_date": "2021-06-30",
|
||||
"sales_order": [so.name],
|
||||
"item": item.item_code,
|
||||
}
|
||||
)
|
||||
|
||||
expected_value = [
|
||||
{
|
||||
"name": so.name,
|
||||
"customer": so.customer,
|
||||
"submitted": datetime.date(2021, 6, 15),
|
||||
"status": "Completed",
|
||||
"payment_term": None,
|
||||
@@ -98,6 +102,7 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
|
||||
},
|
||||
{
|
||||
"name": so.name,
|
||||
"customer": so.customer,
|
||||
"submitted": datetime.date(2021, 6, 15),
|
||||
"status": "Partly Paid",
|
||||
"payment_term": None,
|
||||
@@ -132,11 +137,11 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
|
||||
)
|
||||
doc.insert()
|
||||
|
||||
def test_alternate_currency(self):
|
||||
def test_02_alternate_currency(self):
|
||||
transaction_date = "2021-06-15"
|
||||
self.create_payment_terms_template()
|
||||
self.create_exchange_rate(transaction_date)
|
||||
item = create_item(item_code="_Test Excavator", is_stock_item=0)
|
||||
item = create_item(item_code="_Test Excavator 2", is_stock_item=0)
|
||||
so = make_sales_order(
|
||||
transaction_date=transaction_date,
|
||||
currency="USD",
|
||||
@@ -166,7 +171,7 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
|
||||
"company": "_Test Company",
|
||||
"period_start_date": "2021-06-01",
|
||||
"period_end_date": "2021-06-30",
|
||||
"sales_order": [so.name],
|
||||
"item": item.item_code,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -174,6 +179,7 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
|
||||
expected_value = [
|
||||
{
|
||||
"name": so.name,
|
||||
"customer": so.customer,
|
||||
"submitted": datetime.date(2021, 6, 15),
|
||||
"status": "Completed",
|
||||
"payment_term": None,
|
||||
@@ -187,6 +193,7 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
|
||||
},
|
||||
{
|
||||
"name": so.name,
|
||||
"customer": so.customer,
|
||||
"submitted": datetime.date(2021, 6, 15),
|
||||
"status": "Partly Paid",
|
||||
"payment_term": None,
|
||||
@@ -200,3 +207,134 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
|
||||
},
|
||||
]
|
||||
self.assertEqual(data, expected_value)
|
||||
|
||||
def test_03_group_filters(self):
|
||||
transaction_date = "2021-06-15"
|
||||
self.create_payment_terms_template()
|
||||
item1 = create_item(item_code="_Test Excavator 1", is_stock_item=0)
|
||||
item1.item_group = "Products"
|
||||
item1.save()
|
||||
|
||||
so1 = make_sales_order(
|
||||
transaction_date=transaction_date,
|
||||
delivery_date=add_days(transaction_date, -30),
|
||||
item=item1.item_code,
|
||||
qty=1,
|
||||
rate=1000000,
|
||||
do_not_save=True,
|
||||
)
|
||||
so1.po_no = ""
|
||||
so1.taxes_and_charges = ""
|
||||
so1.taxes = ""
|
||||
so1.payment_terms_template = self.template.name
|
||||
so1.save()
|
||||
so1.submit()
|
||||
|
||||
item2 = create_item(item_code="_Test Steel", is_stock_item=0)
|
||||
item2.item_group = "Raw Material"
|
||||
item2.save()
|
||||
|
||||
so2 = make_sales_order(
|
||||
customer="_Test Customer 1",
|
||||
transaction_date=transaction_date,
|
||||
delivery_date=add_days(transaction_date, -30),
|
||||
item=item2.item_code,
|
||||
qty=100,
|
||||
rate=1000,
|
||||
do_not_save=True,
|
||||
)
|
||||
so2.po_no = ""
|
||||
so2.taxes_and_charges = ""
|
||||
so2.taxes = ""
|
||||
so2.payment_terms_template = self.template.name
|
||||
so2.save()
|
||||
so2.submit()
|
||||
|
||||
base_filters = {
|
||||
"company": "_Test Company",
|
||||
"period_start_date": "2021-06-01",
|
||||
"period_end_date": "2021-06-30",
|
||||
}
|
||||
|
||||
expected_value_so1 = [
|
||||
{
|
||||
"name": so1.name,
|
||||
"customer": so1.customer,
|
||||
"submitted": datetime.date(2021, 6, 15),
|
||||
"status": "Overdue",
|
||||
"payment_term": None,
|
||||
"description": "_Test 50-50",
|
||||
"due_date": datetime.date(2021, 6, 30),
|
||||
"invoice_portion": 50.0,
|
||||
"currency": "INR",
|
||||
"base_payment_amount": 500000.0,
|
||||
"paid_amount": 0.0,
|
||||
"invoices": "",
|
||||
},
|
||||
{
|
||||
"name": so1.name,
|
||||
"customer": so1.customer,
|
||||
"submitted": datetime.date(2021, 6, 15),
|
||||
"status": "Overdue",
|
||||
"payment_term": None,
|
||||
"description": "_Test 50-50",
|
||||
"due_date": datetime.date(2021, 7, 15),
|
||||
"invoice_portion": 50.0,
|
||||
"currency": "INR",
|
||||
"base_payment_amount": 500000.0,
|
||||
"paid_amount": 0.0,
|
||||
"invoices": "",
|
||||
},
|
||||
]
|
||||
|
||||
expected_value_so2 = [
|
||||
{
|
||||
"name": so2.name,
|
||||
"customer": so2.customer,
|
||||
"submitted": datetime.date(2021, 6, 15),
|
||||
"status": "Overdue",
|
||||
"payment_term": None,
|
||||
"description": "_Test 50-50",
|
||||
"due_date": datetime.date(2021, 6, 30),
|
||||
"invoice_portion": 50.0,
|
||||
"currency": "INR",
|
||||
"base_payment_amount": 50000.0,
|
||||
"paid_amount": 0.0,
|
||||
"invoices": "",
|
||||
},
|
||||
{
|
||||
"name": so2.name,
|
||||
"customer": so2.customer,
|
||||
"submitted": datetime.date(2021, 6, 15),
|
||||
"status": "Overdue",
|
||||
"payment_term": None,
|
||||
"description": "_Test 50-50",
|
||||
"due_date": datetime.date(2021, 7, 15),
|
||||
"invoice_portion": 50.0,
|
||||
"currency": "INR",
|
||||
"base_payment_amount": 50000.0,
|
||||
"paid_amount": 0.0,
|
||||
"invoices": "",
|
||||
},
|
||||
]
|
||||
|
||||
group_filters = [
|
||||
{"customer_group": "All Customer Groups"},
|
||||
{"item_group": "All Item Groups"},
|
||||
{"item_group": "Products"},
|
||||
{"item_group": "Raw Material"},
|
||||
]
|
||||
|
||||
expected_values_for_group_filters = [
|
||||
expected_value_so1 + expected_value_so2,
|
||||
expected_value_so1 + expected_value_so2,
|
||||
expected_value_so1,
|
||||
expected_value_so2,
|
||||
]
|
||||
|
||||
for idx, g in enumerate(group_filters, 0):
|
||||
# build filter
|
||||
filters = frappe._dict({}).update(base_filters).update(g)
|
||||
with self.subTest(filters=filters):
|
||||
columns, data, message, chart = execute(filters)
|
||||
self.assertEqual(data, expected_values_for_group_filters[idx])
|
||||
|
||||
@@ -54,6 +54,7 @@ def get_conditions(filters):
|
||||
|
||||
|
||||
def get_data(conditions, filters):
|
||||
# nosemgrep
|
||||
data = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
@@ -65,6 +66,7 @@ def get_data(conditions, filters):
|
||||
IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay,
|
||||
soi.qty, soi.delivered_qty,
|
||||
(soi.qty - soi.delivered_qty) AS pending_qty,
|
||||
IF((SELECT pending_qty) = 0, (TO_SECONDS(Max(dn.posting_date))-TO_SECONDS(so.transaction_date)), 0) as time_taken_to_deliver,
|
||||
IFNULL(SUM(sii.qty), 0) as billed_qty,
|
||||
soi.base_amount as amount,
|
||||
(soi.delivered_qty * soi.base_rate) as delivered_qty_amount,
|
||||
@@ -75,9 +77,13 @@ def get_data(conditions, filters):
|
||||
soi.description as description
|
||||
FROM
|
||||
`tabSales Order` so,
|
||||
`tabSales Order Item` soi
|
||||
(`tabSales Order Item` soi
|
||||
LEFT JOIN `tabSales Invoice Item` sii
|
||||
ON sii.so_detail = soi.name and sii.docstatus = 1
|
||||
ON sii.so_detail = soi.name and sii.docstatus = 1)
|
||||
LEFT JOIN `tabDelivery Note Item` dni
|
||||
on dni.so_detail = soi.name
|
||||
LEFT JOIN `tabDelivery Note` dn
|
||||
on dni.parent = dn.name and dn.docstatus = 1
|
||||
WHERE
|
||||
soi.parent = so.name
|
||||
and so.status not in ('Stopped', 'Closed', 'On Hold')
|
||||
@@ -264,6 +270,12 @@ def get_columns(filters):
|
||||
},
|
||||
{"label": _("Delivery Date"), "fieldname": "delivery_date", "fieldtype": "Date", "width": 120},
|
||||
{"label": _("Delay (in Days)"), "fieldname": "delay", "fieldtype": "Data", "width": 100},
|
||||
{
|
||||
"label": _("Time Taken to Deliver"),
|
||||
"fieldname": "time_taken_to_deliver",
|
||||
"fieldtype": "Duration",
|
||||
"width": 100,
|
||||
},
|
||||
]
|
||||
)
|
||||
if not filters.get("group_by_so"):
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days
|
||||
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note, make_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.selling.report.sales_order_analysis.sales_order_analysis import execute
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Delivery Note"]
|
||||
|
||||
|
||||
class TestSalesOrderAnalysis(FrappeTestCase):
|
||||
def create_sales_order(self, transaction_date):
|
||||
item = create_item(item_code="_Test Excavator", is_stock_item=0)
|
||||
so = make_sales_order(
|
||||
transaction_date=transaction_date,
|
||||
item=item.item_code,
|
||||
qty=10,
|
||||
rate=100000,
|
||||
do_not_save=True,
|
||||
)
|
||||
so.po_no = ""
|
||||
so.taxes_and_charges = ""
|
||||
so.taxes = ""
|
||||
so.items[0].delivery_date = add_days(transaction_date, 15)
|
||||
so.save()
|
||||
so.submit()
|
||||
return item, so
|
||||
|
||||
def create_sales_invoice(self, so):
|
||||
sinv = make_sales_invoice(so.name)
|
||||
sinv.posting_date = so.transaction_date
|
||||
sinv.taxes_and_charges = ""
|
||||
sinv.taxes = ""
|
||||
sinv.insert()
|
||||
sinv.submit()
|
||||
return sinv
|
||||
|
||||
def create_delivery_note(self, so):
|
||||
dn = make_delivery_note(so.name)
|
||||
dn.set_posting_time = True
|
||||
dn.posting_date = add_days(so.transaction_date, 1)
|
||||
dn.save()
|
||||
dn.submit()
|
||||
return dn
|
||||
|
||||
def test_01_so_to_deliver_and_bill(self):
|
||||
transaction_date = "2021-06-01"
|
||||
item, so = self.create_sales_order(transaction_date)
|
||||
columns, data, message, chart = execute(
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"from_date": "2021-06-01",
|
||||
"to_date": "2021-06-30",
|
||||
"status": ["To Deliver and Bill"],
|
||||
}
|
||||
)
|
||||
expected_value = {
|
||||
"status": "To Deliver and Bill",
|
||||
"sales_order": so.name,
|
||||
"delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date),
|
||||
"qty": 10,
|
||||
"delivered_qty": 0,
|
||||
"pending_qty": 10,
|
||||
"qty_to_bill": 10,
|
||||
"time_taken_to_deliver": 0,
|
||||
}
|
||||
self.assertEqual(len(data), 1)
|
||||
for key, val in expected_value.items():
|
||||
with self.subTest(key=key, val=val):
|
||||
self.assertEqual(data[0][key], val)
|
||||
|
||||
def test_02_so_to_deliver(self):
|
||||
transaction_date = "2021-06-01"
|
||||
item, so = self.create_sales_order(transaction_date)
|
||||
self.create_sales_invoice(so)
|
||||
columns, data, message, chart = execute(
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"from_date": "2021-06-01",
|
||||
"to_date": "2021-06-30",
|
||||
"status": ["To Deliver"],
|
||||
}
|
||||
)
|
||||
expected_value = {
|
||||
"status": "To Deliver",
|
||||
"sales_order": so.name,
|
||||
"delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date),
|
||||
"qty": 10,
|
||||
"delivered_qty": 0,
|
||||
"pending_qty": 10,
|
||||
"qty_to_bill": 0,
|
||||
"time_taken_to_deliver": 0,
|
||||
}
|
||||
self.assertEqual(len(data), 1)
|
||||
for key, val in expected_value.items():
|
||||
with self.subTest(key=key, val=val):
|
||||
self.assertEqual(data[0][key], val)
|
||||
|
||||
def test_03_so_to_bill(self):
|
||||
transaction_date = "2021-06-01"
|
||||
item, so = self.create_sales_order(transaction_date)
|
||||
self.create_delivery_note(so)
|
||||
columns, data, message, chart = execute(
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"from_date": "2021-06-01",
|
||||
"to_date": "2021-06-30",
|
||||
"status": ["To Bill"],
|
||||
}
|
||||
)
|
||||
expected_value = {
|
||||
"status": "To Bill",
|
||||
"sales_order": so.name,
|
||||
"delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date),
|
||||
"qty": 10,
|
||||
"delivered_qty": 10,
|
||||
"pending_qty": 0,
|
||||
"qty_to_bill": 10,
|
||||
"time_taken_to_deliver": 86400,
|
||||
}
|
||||
self.assertEqual(len(data), 1)
|
||||
for key, val in expected_value.items():
|
||||
with self.subTest(key=key, val=val):
|
||||
self.assertEqual(data[0][key], val)
|
||||
|
||||
def test_04_so_completed(self):
|
||||
transaction_date = "2021-06-01"
|
||||
item, so = self.create_sales_order(transaction_date)
|
||||
self.create_sales_invoice(so)
|
||||
self.create_delivery_note(so)
|
||||
columns, data, message, chart = execute(
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"from_date": "2021-06-01",
|
||||
"to_date": "2021-06-30",
|
||||
"status": ["Completed"],
|
||||
}
|
||||
)
|
||||
expected_value = {
|
||||
"status": "Completed",
|
||||
"sales_order": so.name,
|
||||
"delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date),
|
||||
"qty": 10,
|
||||
"delivered_qty": 10,
|
||||
"pending_qty": 0,
|
||||
"qty_to_bill": 0,
|
||||
"billed_qty": 10,
|
||||
"time_taken_to_deliver": 86400,
|
||||
}
|
||||
self.assertEqual(len(data), 1)
|
||||
for key, val in expected_value.items():
|
||||
with self.subTest(key=key, val=val):
|
||||
self.assertEqual(data[0][key], val)
|
||||
|
||||
def test_05_all_so_status(self):
|
||||
columns, data, message, chart = execute(
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"from_date": "2021-06-01",
|
||||
"to_date": "2021-06-30",
|
||||
}
|
||||
)
|
||||
# SO's from first 4 test cases should be in output
|
||||
self.assertEqual(len(data), 4)
|
||||
@@ -233,7 +233,8 @@ erpnext.company.setup_queries = function(frm) {
|
||||
["expenses_included_in_asset_valuation", {"account_type": "Expenses Included In Asset Valuation"}],
|
||||
["capital_work_in_progress_account", {"account_type": "Capital Work in Progress"}],
|
||||
["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}],
|
||||
["unrealized_profit_loss_account", {"root_type": ["in", ["Liability", "Asset"]]}]
|
||||
["unrealized_profit_loss_account", {"root_type": ["in", ["Liability", "Asset"]]}],
|
||||
["default_provisional_account", {"root_type": ["in", ["Liability", "Asset"]]}]
|
||||
], function(i, v) {
|
||||
erpnext.company.set_custom_query(frm, v);
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user