mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-28 13:28:35 +00:00
Compare commits
109 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3efa5215a0 | ||
|
|
1fa0fe7434 | ||
|
|
be48b4a028 | ||
|
|
b6ed0698b4 | ||
|
|
208d541373 | ||
|
|
865f233add | ||
|
|
ea99ac9c29 | ||
|
|
6516e8042b | ||
|
|
3c83ec5613 | ||
|
|
e5a187e08c | ||
|
|
8df11516be | ||
|
|
03af48b50b | ||
|
|
a2bd8d22cb | ||
|
|
d2f86ead74 | ||
|
|
985c47fe3b | ||
|
|
6c7815cd72 | ||
|
|
9b1384f79c | ||
|
|
299e02ae46 | ||
|
|
4fb2d2c000 | ||
|
|
c9bf062f63 | ||
|
|
196ba6759e | ||
|
|
66ba098462 | ||
|
|
4ba2f1ec96 | ||
|
|
6ab91348fd | ||
|
|
3f9b8923a9 | ||
|
|
038fa4dbe2 | ||
|
|
d97e673874 | ||
|
|
b16bfa541d | ||
|
|
8156cb4db0 | ||
|
|
65688684a7 | ||
|
|
e01ff0df40 | ||
|
|
f0475e9cc5 | ||
|
|
b2333d3b5f | ||
|
|
8521e12753 | ||
|
|
1c7c591ee2 | ||
|
|
c18a451362 | ||
|
|
529fb411ab | ||
|
|
4acaa3eecf | ||
|
|
cf133b2f1c | ||
|
|
92c895aa1c | ||
|
|
8d99763bea | ||
|
|
b4589d8b8f | ||
|
|
ec15965a6c | ||
|
|
c850635551 | ||
|
|
acf8b464f3 | ||
|
|
37ae2dfe7f | ||
|
|
5ec11bad4f | ||
|
|
6e363a62db | ||
|
|
334219e36a | ||
|
|
cbda28d739 | ||
|
|
79643a5716 | ||
|
|
707eb9e8d5 | ||
|
|
19feebbcb6 | ||
|
|
0cf9702a21 | ||
|
|
64454e0d4e | ||
|
|
7b813d6045 | ||
|
|
bc04e05b46 | ||
|
|
c3b9059c1b | ||
|
|
7f1574478b | ||
|
|
b634e44fac | ||
|
|
3aa335d36a | ||
|
|
820e82265b | ||
|
|
91dad909ec | ||
|
|
1068d0ec63 | ||
|
|
ae3581c7d0 | ||
|
|
7a97cc4629 | ||
|
|
8e1c0cd234 | ||
|
|
d58719a30a | ||
|
|
6a394c5be7 | ||
|
|
35c71f5923 | ||
|
|
52d4e2acf8 | ||
|
|
3691eec66c | ||
|
|
e93af962a1 | ||
|
|
9692eeeb15 | ||
|
|
7825c564eb | ||
|
|
e5fd95bb21 | ||
|
|
684a45f234 | ||
|
|
7ef0c6bb01 | ||
|
|
4587bb3767 | ||
|
|
2a18067aad | ||
|
|
b573d9739f | ||
|
|
4dbce87660 | ||
|
|
b82154cb9e | ||
|
|
2105193594 | ||
|
|
ef74c6689b | ||
|
|
799d7b254e | ||
|
|
086e74791b | ||
|
|
c3fca3cfcb | ||
|
|
1634448864 | ||
|
|
a6241fc813 | ||
|
|
896bac10bc | ||
|
|
0992ca40b0 | ||
|
|
10ac8d6e67 | ||
|
|
c8a2f9f857 | ||
|
|
b13ee4fc8c | ||
|
|
ab2f250960 | ||
|
|
e5b3748b49 | ||
|
|
eddb7b429e | ||
|
|
b3a9c1eb2a | ||
|
|
e44646054c | ||
|
|
12f11a6976 | ||
|
|
50b2898a2c | ||
|
|
23830266f2 | ||
|
|
b17ac522f5 | ||
|
|
1b22f53fde | ||
|
|
988a327b58 | ||
|
|
45f79ef644 | ||
|
|
bce3506e90 | ||
|
|
2880469706 |
@@ -4,7 +4,7 @@ import frappe
|
||||
|
||||
from erpnext.hooks import regional_overrides
|
||||
|
||||
__version__ = "13.42.3"
|
||||
__version__ = "13.43.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -378,7 +378,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
return
|
||||
|
||||
# check if books nor frozen till endate:
|
||||
if accounts_frozen_upto and (end_date) <= getdate(accounts_frozen_upto):
|
||||
if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto):
|
||||
end_date = get_last_day(add_days(accounts_frozen_upto, 1))
|
||||
|
||||
if via_journal_entry:
|
||||
|
||||
@@ -299,7 +299,7 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
|
||||
dict(
|
||||
account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"]
|
||||
),
|
||||
["credit", "debit"],
|
||||
["credit_in_account_currency as credit", "debit_in_account_currency as debit"],
|
||||
as_dict=1,
|
||||
)
|
||||
gl_amount, transaction_amount = (
|
||||
|
||||
@@ -138,7 +138,7 @@ def get_paid_amount(payment_entry, currency, bank_account):
|
||||
)
|
||||
elif doc.payment_type == "Pay":
|
||||
paid_amount_field = (
|
||||
"paid_amount" if doc.paid_to_account_currency == currency else "base_paid_amount"
|
||||
"paid_amount" if doc.paid_from_account_currency == currency else "base_paid_amount"
|
||||
)
|
||||
|
||||
return frappe.db.get_value(
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Type",
|
||||
"options": "DocType",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
@@ -35,7 +36,8 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Name",
|
||||
"options": "reference_doctype",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "due_date",
|
||||
@@ -104,7 +106,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-26 17:06:55.597389",
|
||||
"modified": "2022-12-12 12:31:44.919895",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Reference",
|
||||
@@ -113,5 +115,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -3,8 +3,10 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe import _, msgprint, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import flt, getdate, nowdate, today
|
||||
|
||||
import erpnext
|
||||
@@ -120,58 +122,79 @@ class PaymentReconciliation(Document):
|
||||
return list(journal_entries)
|
||||
|
||||
def get_dr_or_cr_notes(self):
|
||||
condition = self.get_conditions(get_return_invoices=True)
|
||||
gl = qb.DocType("GL Entry")
|
||||
|
||||
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
doc = qb.DocType(voucher_type)
|
||||
|
||||
# build conditions
|
||||
sub_query_conditions = []
|
||||
conditions = []
|
||||
sub_query_conditions.append(doc.company == self.company)
|
||||
|
||||
if self.get("from_payment_date"):
|
||||
sub_query_conditions.append(doc.posting_date.gte(self.from_payment_date))
|
||||
|
||||
if self.get("to_payment_date"):
|
||||
sub_query_conditions.append(doc.posting_date.lte(self.to_payment_date))
|
||||
|
||||
if self.get("cost_center"):
|
||||
condition += " and doc.cost_center = '{0}' ".format(self.cost_center)
|
||||
sub_query_conditions.append(doc.cost_center == self.cost_center)
|
||||
|
||||
dr_or_cr = (
|
||||
"credit_in_account_currency"
|
||||
gl["credit_in_account_currency"]
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
else "debit_in_account_currency"
|
||||
else gl["debit_in_account_currency"]
|
||||
)
|
||||
|
||||
reconciled_dr_or_cr = (
|
||||
"debit_in_account_currency"
|
||||
if dr_or_cr == "credit_in_account_currency"
|
||||
else "credit_in_account_currency"
|
||||
gl["debit_in_account_currency"]
|
||||
if dr_or_cr.name == "credit_in_account_currency"
|
||||
else gl["credit_in_account_currency"]
|
||||
)
|
||||
|
||||
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
having_clause = qb.Field("amount") > 0
|
||||
|
||||
return frappe.db.sql(
|
||||
""" SELECT doc.name as reference_name, %(voucher_type)s as reference_type,
|
||||
(sum(gl.{dr_or_cr}) - sum(gl.{reconciled_dr_or_cr})) as amount, doc.posting_date,
|
||||
account_currency as currency
|
||||
FROM `tab{doc}` doc, `tabGL Entry` gl
|
||||
WHERE
|
||||
(doc.name = gl.against_voucher or doc.name = gl.voucher_no)
|
||||
and doc.{party_type_field} = %(party)s
|
||||
and doc.is_return = 1 and ifnull(doc.return_against, "") = ""
|
||||
and gl.against_voucher_type = %(voucher_type)s
|
||||
and doc.docstatus = 1 and gl.party = %(party)s
|
||||
and gl.party_type = %(party_type)s and gl.account = %(account)s
|
||||
and gl.is_cancelled = 0 {condition}
|
||||
GROUP BY doc.name
|
||||
Having
|
||||
amount > 0
|
||||
ORDER BY doc.posting_date
|
||||
""".format(
|
||||
doc=voucher_type,
|
||||
dr_or_cr=dr_or_cr,
|
||||
reconciled_dr_or_cr=reconciled_dr_or_cr,
|
||||
party_type_field=frappe.scrub(self.party_type),
|
||||
condition=condition or "",
|
||||
),
|
||||
{
|
||||
"party": self.party,
|
||||
"party_type": self.party_type,
|
||||
"voucher_type": voucher_type,
|
||||
"account": self.receivable_payable_account,
|
||||
},
|
||||
as_dict=1,
|
||||
if self.minimum_payment_amount:
|
||||
having_clause = qb.Field("amount") >= self.minimum_payment_amount
|
||||
if self.maximum_payment_amount:
|
||||
having_clause = having_clause & qb.Field("amount") <= self.maximum_payment_amount
|
||||
|
||||
sub_query = (
|
||||
qb.from_(doc)
|
||||
.select(doc.name)
|
||||
.where(Criterion.all(sub_query_conditions))
|
||||
.where(
|
||||
(doc.docstatus == 1)
|
||||
& (doc.is_return == 1)
|
||||
& ((doc.return_against == "") | (doc.return_against.isnull()))
|
||||
)
|
||||
)
|
||||
|
||||
query = (
|
||||
qb.from_(gl)
|
||||
.select(
|
||||
gl.against_voucher_type.as_("reference_type"),
|
||||
gl.against_voucher.as_("reference_name"),
|
||||
(Sum(dr_or_cr) - Sum(reconciled_dr_or_cr)).as_("amount"),
|
||||
gl.posting_date,
|
||||
gl.account_currency.as_("currency"),
|
||||
)
|
||||
.where(
|
||||
(gl.against_voucher.isin(sub_query))
|
||||
& (gl.against_voucher_type == voucher_type)
|
||||
& (gl.is_cancelled == 0)
|
||||
& (gl.account == self.receivable_payable_account)
|
||||
& (gl.party_type == self.party_type)
|
||||
& (gl.party == self.party)
|
||||
)
|
||||
.where(Criterion.all(conditions))
|
||||
.groupby(gl.against_voucher)
|
||||
.having(having_clause)
|
||||
)
|
||||
dr_cr_notes = query.run(as_dict=True)
|
||||
return dr_cr_notes
|
||||
|
||||
def add_payment_entries(self, non_reconciled_payments):
|
||||
self.set("payments", [])
|
||||
|
||||
@@ -369,7 +392,7 @@ class PaymentReconciliation(Document):
|
||||
if not invoices_to_reconcile:
|
||||
frappe.throw(_("No records found in Allocation table"))
|
||||
|
||||
def get_conditions(self, get_invoices=False, get_payments=False, get_return_invoices=False):
|
||||
def get_conditions(self, get_invoices=False, get_payments=False):
|
||||
condition = " and company = '{0}' ".format(self.company)
|
||||
|
||||
if get_invoices:
|
||||
@@ -397,35 +420,7 @@ class PaymentReconciliation(Document):
|
||||
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)
|
||||
condition += (
|
||||
" and doc.posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
|
||||
if self.from_payment_date
|
||||
else ""
|
||||
)
|
||||
condition += (
|
||||
" and doc.posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
|
||||
if self.to_payment_date
|
||||
else ""
|
||||
)
|
||||
dr_or_cr = (
|
||||
"debit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
else "credit_in_account_currency"
|
||||
)
|
||||
|
||||
if self.minimum_invoice_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 gl.{dr_or_cr} <= {amount}".format(
|
||||
dr_or_cr=dr_or_cr, amount=flt(self.maximum_payment_amount)
|
||||
)
|
||||
|
||||
else:
|
||||
elif get_payments:
|
||||
condition += (
|
||||
" and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
|
||||
if self.from_payment_date
|
||||
|
||||
@@ -843,7 +843,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-10-10 20:57:38.340026",
|
||||
"modified": "2022-12-28 16:17:33.484531",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
||||
@@ -36,11 +36,11 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-09-13 23:40:41.479208",
|
||||
"modified": "2022-12-28 23:40:41.479208",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withheld Vouchers",
|
||||
"naming_rule": "Autoincrement",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _, scrub
|
||||
from frappe import _, qb, scrub
|
||||
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -95,6 +95,9 @@ class ReceivablePayableReport(object):
|
||||
# Get return entries
|
||||
self.get_return_entries()
|
||||
|
||||
# Get Exchange Rate Revaluations
|
||||
self.get_exchange_rate_revaluations()
|
||||
|
||||
self.data = []
|
||||
for gle in self.gl_entries:
|
||||
self.update_voucher_balance(gle)
|
||||
@@ -258,7 +261,8 @@ class ReceivablePayableReport(object):
|
||||
row.invoice_grand_total = row.invoiced
|
||||
|
||||
if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and (
|
||||
abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision
|
||||
(abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision)
|
||||
or (row.voucher_no in self.err_journals)
|
||||
):
|
||||
# non-zero oustanding, we must consider this row
|
||||
|
||||
@@ -1033,3 +1037,17 @@ class ReceivablePayableReport(object):
|
||||
"data": {"labels": self.ageing_column_labels, "datasets": rows},
|
||||
"type": "percentage",
|
||||
}
|
||||
|
||||
def get_exchange_rate_revaluations(self):
|
||||
je = qb.DocType("Journal Entry")
|
||||
results = (
|
||||
qb.from_(je)
|
||||
.select(je.name)
|
||||
.where(
|
||||
(je.company == self.filters.company)
|
||||
& (je.posting_date.lte(self.filters.report_date))
|
||||
& (je.voucher_type == "Exchange Rate Revaluation")
|
||||
)
|
||||
.run()
|
||||
)
|
||||
self.err_journals = [x[0] for x in results] if results else []
|
||||
|
||||
@@ -1,18 +1,51 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, getdate, today
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, getdate, today
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute
|
||||
|
||||
|
||||
class TestAccountsReceivable(unittest.TestCase):
|
||||
def test_accounts_receivable(self):
|
||||
def setUp(self):
|
||||
frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'")
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'")
|
||||
frappe.db.sql("delete from `tabJournal Entry` where company='_Test Company 2'")
|
||||
frappe.db.sql("delete from `tabExchange Rate Revaluation` where company='_Test Company 2'")
|
||||
|
||||
self.create_usd_account()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_usd_account(self):
|
||||
name = "Debtors USD"
|
||||
exists = frappe.db.get_list(
|
||||
"Account", filters={"company": "_Test Company 2", "account_name": "Debtors USD"}
|
||||
)
|
||||
if exists:
|
||||
self.debtors_usd = exists[0].name
|
||||
else:
|
||||
debtors = frappe.get_doc(
|
||||
"Account",
|
||||
frappe.db.get_list(
|
||||
"Account", filters={"company": "_Test Company 2", "account_name": "Debtors"}
|
||||
)[0].name,
|
||||
)
|
||||
|
||||
debtors_usd = frappe.new_doc("Account")
|
||||
debtors_usd.company = debtors.company
|
||||
debtors_usd.account_name = "Debtors USD"
|
||||
debtors_usd.account_currency = "USD"
|
||||
debtors_usd.parent_account = debtors.parent_account
|
||||
debtors_usd.account_type = debtors.account_type
|
||||
self.debtors_usd = debtors_usd.save().name
|
||||
|
||||
def test_accounts_receivable(self):
|
||||
filters = {
|
||||
"company": "_Test Company 2",
|
||||
"based_on_payment_terms": 1,
|
||||
@@ -24,7 +57,7 @@ class TestAccountsReceivable(unittest.TestCase):
|
||||
}
|
||||
|
||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||
name = make_sales_invoice()
|
||||
name = make_sales_invoice().name
|
||||
report = execute(filters)
|
||||
|
||||
expected_data = [[100, 30], [100, 50], [100, 20]]
|
||||
@@ -65,8 +98,74 @@ class TestAccountsReceivable(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1}
|
||||
)
|
||||
def test_exchange_revaluation_for_party(self):
|
||||
"""
|
||||
Exchange Revaluation for party on Receivable/Payable shoule be included
|
||||
"""
|
||||
|
||||
def make_sales_invoice():
|
||||
company = "_Test Company 2"
|
||||
customer = "_Test Customer 2"
|
||||
|
||||
# Using Exchange Gain/Loss account for unrealized as well.
|
||||
company_doc = frappe.get_doc("Company", company)
|
||||
company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account
|
||||
company_doc.save()
|
||||
|
||||
si = make_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
si.currency = "USD"
|
||||
si.conversion_rate = 0.90
|
||||
si.debit_to = self.debtors_usd
|
||||
si = si.save().submit()
|
||||
|
||||
# Exchange Revaluation
|
||||
err = frappe.new_doc("Exchange Rate Revaluation")
|
||||
err.company = company
|
||||
err.posting_date = today()
|
||||
accounts = err.get_accounts_data()
|
||||
err.extend("accounts", accounts)
|
||||
err.accounts[0].new_exchange_rate = 0.95
|
||||
row = err.accounts[0]
|
||||
row.new_balance_in_base_currency = flt(
|
||||
row.new_exchange_rate * flt(row.balance_in_account_currency)
|
||||
)
|
||||
row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency)
|
||||
err.set_total_gain_loss()
|
||||
err = err.save().submit()
|
||||
|
||||
# Submit JV for ERR
|
||||
jv = frappe.get_doc(err.make_jv_entry())
|
||||
jv = jv.save()
|
||||
for x in jv.accounts:
|
||||
x.cost_center = get_default_cost_center(jv.company)
|
||||
jv.submit()
|
||||
|
||||
filters = {
|
||||
"company": company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_for_err = [0, -5, 0, 5]
|
||||
row = [x for x in report[1] if x.voucher_type == jv.doctype and x.voucher_no == jv.name][0]
|
||||
self.assertEqual(
|
||||
expected_data_for_err,
|
||||
[
|
||||
row.invoiced,
|
||||
row.paid,
|
||||
row.credit_note,
|
||||
row.outstanding,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def make_sales_invoice(no_payment_schedule=False, do_not_submit=False):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
si = create_sales_invoice(
|
||||
@@ -81,22 +180,26 @@ def make_sales_invoice():
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
si.append(
|
||||
"payment_schedule",
|
||||
dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
|
||||
)
|
||||
si.append(
|
||||
"payment_schedule",
|
||||
dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50),
|
||||
)
|
||||
si.append(
|
||||
"payment_schedule",
|
||||
dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20),
|
||||
)
|
||||
if not no_payment_schedule:
|
||||
si.append(
|
||||
"payment_schedule",
|
||||
dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
|
||||
)
|
||||
si.append(
|
||||
"payment_schedule",
|
||||
dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50),
|
||||
)
|
||||
si.append(
|
||||
"payment_schedule",
|
||||
dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20),
|
||||
)
|
||||
|
||||
si.submit()
|
||||
si = si.save()
|
||||
|
||||
return si.name
|
||||
if not do_not_submit:
|
||||
si = si.submit()
|
||||
|
||||
return si
|
||||
|
||||
|
||||
def make_payment(docname):
|
||||
|
||||
@@ -22,8 +22,7 @@ def get_columns():
|
||||
{
|
||||
"label": _("Payment Document Type"),
|
||||
"fieldname": "payment_document_type",
|
||||
"fieldtype": "Link",
|
||||
"options": "Doctype",
|
||||
"fieldtype": "Data",
|
||||
"width": 130,
|
||||
},
|
||||
{
|
||||
@@ -33,15 +32,15 @@ def get_columns():
|
||||
"options": "payment_document_type",
|
||||
"width": 140,
|
||||
},
|
||||
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
|
||||
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120},
|
||||
{"label": _("Cheque/Reference No"), "fieldname": "cheque_no", "width": 120},
|
||||
{"label": _("Clearance Date"), "fieldname": "clearance_date", "fieldtype": "Date", "width": 100},
|
||||
{"label": _("Clearance Date"), "fieldname": "clearance_date", "fieldtype": "Date", "width": 120},
|
||||
{
|
||||
"label": _("Against Account"),
|
||||
"fieldname": "against",
|
||||
"fieldtype": "Link",
|
||||
"options": "Account",
|
||||
"width": 170,
|
||||
"width": 200,
|
||||
},
|
||||
{"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120},
|
||||
]
|
||||
|
||||
@@ -9,6 +9,7 @@ from six import iteritems
|
||||
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
get_columns,
|
||||
get_cost_centers_with_children,
|
||||
get_data,
|
||||
get_filtered_list_for_consolidated_report,
|
||||
get_period_list,
|
||||
@@ -161,10 +162,11 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_
|
||||
total = 0
|
||||
for period in period_list:
|
||||
start_date = get_start_date(period, accumulated_values, company)
|
||||
filters.start_date = start_date
|
||||
filters.end_date = period["to_date"]
|
||||
filters.account_type = account_type
|
||||
|
||||
amount = get_account_type_based_gl_data(
|
||||
company, start_date, period["to_date"], account_type, filters
|
||||
)
|
||||
amount = get_account_type_based_gl_data(company, filters)
|
||||
|
||||
if amount and account_type == "Depreciation":
|
||||
amount *= -1
|
||||
@@ -176,7 +178,7 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_
|
||||
return data
|
||||
|
||||
|
||||
def get_account_type_based_gl_data(company, start_date, end_date, account_type, filters=None):
|
||||
def get_account_type_based_gl_data(company, filters=None):
|
||||
cond = ""
|
||||
filters = frappe._dict(filters or {})
|
||||
|
||||
@@ -192,17 +194,21 @@ def get_account_type_based_gl_data(company, start_date, end_date, account_type,
|
||||
frappe.db.escape(cstr(filters.finance_book))
|
||||
)
|
||||
|
||||
if filters.get("cost_center"):
|
||||
filters.cost_center = get_cost_centers_with_children(filters.cost_center)
|
||||
cond += " and cost_center in %(cost_center)s"
|
||||
|
||||
gl_sum = frappe.db.sql_list(
|
||||
"""
|
||||
select sum(credit) - sum(debit)
|
||||
from `tabGL Entry`
|
||||
where company=%s and posting_date >= %s and posting_date <= %s
|
||||
where company=%(company)s and posting_date >= %(start_date)s and posting_date <= %(end_date)s
|
||||
and voucher_type != 'Period Closing Voucher'
|
||||
and account in ( SELECT name FROM tabAccount WHERE account_type = %s) {cond}
|
||||
and account in ( SELECT name FROM tabAccount WHERE account_type = %(account_type)s) {cond}
|
||||
""".format(
|
||||
cond=cond
|
||||
),
|
||||
(company, start_date, end_date, account_type),
|
||||
filters,
|
||||
)
|
||||
|
||||
return gl_sum[0] if gl_sum and gl_sum[0] else 0
|
||||
|
||||
@@ -268,10 +268,12 @@ def get_cash_flow_data(fiscal_year, companies, filters):
|
||||
def get_account_type_based_data(account_type, companies, fiscal_year, filters):
|
||||
data = {}
|
||||
total = 0
|
||||
filters.account_type = account_type
|
||||
filters.start_date = fiscal_year.year_start_date
|
||||
filters.end_date = fiscal_year.year_end_date
|
||||
|
||||
for company in companies:
|
||||
amount = get_account_type_based_gl_data(
|
||||
company, fiscal_year.year_start_date, fiscal_year.year_end_date, account_type, filters
|
||||
)
|
||||
amount = get_account_type_based_gl_data(company, filters)
|
||||
|
||||
if amount and account_type == "Depreciation":
|
||||
amount *= -1
|
||||
@@ -533,12 +535,13 @@ def get_accounts(root_type, companies):
|
||||
],
|
||||
filters={"company": company, "root_type": root_type},
|
||||
):
|
||||
if account.account_name not in added_accounts:
|
||||
if account.account_number:
|
||||
account_key = account.account_number + "-" + account.account_name
|
||||
else:
|
||||
account_key = account.account_name
|
||||
|
||||
if account_key not in added_accounts:
|
||||
accounts.append(account)
|
||||
if account.account_number:
|
||||
account_key = account.account_number + "-" + account.account_name
|
||||
else:
|
||||
account_key = account.account_name
|
||||
added_accounts.append(account_key)
|
||||
|
||||
return accounts
|
||||
|
||||
@@ -27,6 +27,7 @@ class PartyLedgerSummaryReport(object):
|
||||
)
|
||||
|
||||
self.get_gl_entries()
|
||||
self.get_additional_columns()
|
||||
self.get_return_invoices()
|
||||
self.get_party_adjustment_amounts()
|
||||
|
||||
@@ -34,6 +35,42 @@ class PartyLedgerSummaryReport(object):
|
||||
data = self.get_data()
|
||||
return columns, data
|
||||
|
||||
def get_additional_columns(self):
|
||||
"""
|
||||
Additional Columns for 'User Permission' based access control
|
||||
"""
|
||||
from frappe import qb
|
||||
|
||||
if self.filters.party_type == "Customer":
|
||||
self.territories = frappe._dict({})
|
||||
self.customer_group = frappe._dict({})
|
||||
|
||||
customer = qb.DocType("Customer")
|
||||
result = (
|
||||
frappe.qb.from_(customer)
|
||||
.select(
|
||||
customer.name, customer.territory, customer.customer_group, customer.default_sales_partner
|
||||
)
|
||||
.where((customer.disabled == 0))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
for x in result:
|
||||
self.territories[x.name] = x.territory
|
||||
self.customer_group[x.name] = x.customer_group
|
||||
else:
|
||||
self.supplier_group = frappe._dict({})
|
||||
supplier = qb.DocType("Supplier")
|
||||
result = (
|
||||
frappe.qb.from_(supplier)
|
||||
.select(supplier.name, supplier.supplier_group)
|
||||
.where((supplier.disabled == 0))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
for x in result:
|
||||
self.supplier_group[x.name] = x.supplier_group
|
||||
|
||||
def get_columns(self):
|
||||
columns = [
|
||||
{
|
||||
@@ -117,6 +154,35 @@ class PartyLedgerSummaryReport(object):
|
||||
},
|
||||
]
|
||||
|
||||
# Hidden columns for handling 'User Permissions'
|
||||
if self.filters.party_type == "Customer":
|
||||
columns += [
|
||||
{
|
||||
"label": _("Territory"),
|
||||
"fieldname": "territory",
|
||||
"fieldtype": "Link",
|
||||
"options": "Territory",
|
||||
"hidden": 1,
|
||||
},
|
||||
{
|
||||
"label": _("Customer Group"),
|
||||
"fieldname": "customer_group",
|
||||
"fieldtype": "Link",
|
||||
"options": "Customer Group",
|
||||
"hidden": 1,
|
||||
},
|
||||
]
|
||||
else:
|
||||
columns += [
|
||||
{
|
||||
"label": _("Supplier Group"),
|
||||
"fieldname": "supplier_group",
|
||||
"fieldtype": "Link",
|
||||
"options": "Supplier Group",
|
||||
"hidden": 1,
|
||||
}
|
||||
]
|
||||
|
||||
return columns
|
||||
|
||||
def get_data(self):
|
||||
@@ -144,6 +210,12 @@ class PartyLedgerSummaryReport(object):
|
||||
),
|
||||
)
|
||||
|
||||
if self.filters.party_type == "Customer":
|
||||
self.party_data[gle.party].update({"territory": self.territories.get(gle.party)})
|
||||
self.party_data[gle.party].update({"customer_group": self.customer_group.get(gle.party)})
|
||||
else:
|
||||
self.party_data[gle.party].update({"supplier_group": self.supplier_group.get(gle.party)})
|
||||
|
||||
amount = gle.get(invoice_dr_or_cr) - gle.get(reverse_dr_or_cr)
|
||||
self.party_data[gle.party].closing_balance += amount
|
||||
|
||||
|
||||
@@ -282,7 +282,7 @@ def get_conditions(filters):
|
||||
):
|
||||
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
|
||||
|
||||
conditions.append("(posting_date <=%(to_date)s)")
|
||||
conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')")
|
||||
|
||||
if filters.get("project"):
|
||||
conditions.append("project in %(project)s")
|
||||
|
||||
@@ -551,6 +551,7 @@ class GrossProfitGenerator(object):
|
||||
return abs(previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty))
|
||||
else:
|
||||
return flt(row.qty) * self.get_average_buying_rate(row, item_code)
|
||||
return 0.0
|
||||
|
||||
def get_buying_amount(self, row, item_code):
|
||||
# IMP NOTE
|
||||
|
||||
@@ -6,6 +6,8 @@ from frappe.utils import add_days, flt, nowdate
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.gross_profit.gross_profit import execute
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
@@ -14,6 +16,7 @@ class TestGrossProfit(FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_item()
|
||||
self.create_bundle()
|
||||
self.create_customer()
|
||||
self.create_sales_invoice()
|
||||
self.clear_old_entries()
|
||||
@@ -42,6 +45,7 @@ class TestGrossProfit(FrappeTestCase):
|
||||
self.company = company.name
|
||||
self.cost_center = company.cost_center
|
||||
self.warehouse = "Stores - " + abbr
|
||||
self.finished_warehouse = "Finished Goods - " + abbr
|
||||
self.income_account = "Sales - " + abbr
|
||||
self.expense_account = "Cost of Goods Sold - " + abbr
|
||||
self.debit_to = "Debtors - " + abbr
|
||||
@@ -53,6 +57,23 @@ class TestGrossProfit(FrappeTestCase):
|
||||
)
|
||||
self.item = item if isinstance(item, str) else item.item_code
|
||||
|
||||
def create_bundle(self):
|
||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||
|
||||
item2 = create_item(
|
||||
item_code="_Test GP Item 2", is_stock_item=1, company=self.company, warehouse=self.warehouse
|
||||
)
|
||||
self.item2 = item2 if isinstance(item2, str) else item2.item_code
|
||||
|
||||
# This will be parent item
|
||||
bundle = create_item(
|
||||
item_code="_Test GP bundle", is_stock_item=0, company=self.company, warehouse=self.warehouse
|
||||
)
|
||||
self.bundle = bundle if isinstance(bundle, str) else bundle.item_code
|
||||
|
||||
# Create Product Bundle
|
||||
self.product_bundle = make_product_bundle(parent=self.bundle, items=[self.item, self.item2])
|
||||
|
||||
def create_customer(self):
|
||||
name = "_Test GP Customer"
|
||||
if frappe.db.exists("Customer", name):
|
||||
@@ -93,6 +114,28 @@ class TestGrossProfit(FrappeTestCase):
|
||||
)
|
||||
return sinv
|
||||
|
||||
def create_delivery_note(
|
||||
self, item=None, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
||||
):
|
||||
"""
|
||||
Helper function to populate default values in Delivery Note
|
||||
"""
|
||||
dnote = create_delivery_note(
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
currency="INR",
|
||||
item=item or self.item,
|
||||
qty=qty,
|
||||
rate=rate,
|
||||
cost_center=self.cost_center,
|
||||
warehouse=self.warehouse,
|
||||
return_against=None,
|
||||
expense_account=self.expense_account,
|
||||
do_not_save=do_not_save,
|
||||
do_not_submit=do_not_submit,
|
||||
)
|
||||
return dnote
|
||||
|
||||
def clear_old_entries(self):
|
||||
doctype_list = [
|
||||
"Sales Invoice",
|
||||
@@ -206,3 +249,55 @@ class TestGrossProfit(FrappeTestCase):
|
||||
}
|
||||
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||
self.assertDictContainsSubset(expected_entry_with_dn, gp_entry[0])
|
||||
|
||||
def test_bundled_delivery_note_with_different_warehouses(self):
|
||||
"""
|
||||
Test Delivery Note with bundled item. Packed Item from the bundle having different warehouses
|
||||
"""
|
||||
se = make_stock_entry(
|
||||
company=self.company,
|
||||
item_code=self.item,
|
||||
target=self.warehouse,
|
||||
qty=1,
|
||||
basic_rate=100,
|
||||
do_not_submit=True,
|
||||
)
|
||||
item = se.items[0]
|
||||
se.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": self.item2,
|
||||
"s_warehouse": "",
|
||||
"t_warehouse": self.finished_warehouse,
|
||||
"qty": 1,
|
||||
"basic_rate": 100,
|
||||
"conversion_factor": item.conversion_factor or 1.0,
|
||||
"transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0),
|
||||
"serial_no": item.serial_no,
|
||||
"batch_no": item.batch_no,
|
||||
"cost_center": item.cost_center,
|
||||
"expense_account": item.expense_account,
|
||||
},
|
||||
)
|
||||
se = se.save().submit()
|
||||
|
||||
# Make a Delivery note with Product bundle
|
||||
# Packed Items will have different warehouses
|
||||
dnote = self.create_delivery_note(item=self.bundle, qty=1, rate=200, do_not_submit=True)
|
||||
dnote.packed_items[1].warehouse = self.finished_warehouse
|
||||
dnote = dnote.submit()
|
||||
|
||||
# make Sales Invoice for above delivery note
|
||||
sinv = make_sales_invoice(dnote.name)
|
||||
sinv = sinv.save().submit()
|
||||
|
||||
filters = frappe._dict(
|
||||
company=self.company,
|
||||
from_date=nowdate(),
|
||||
to_date=nowdate(),
|
||||
group_by="Invoice",
|
||||
sales_invoice=sinv.name,
|
||||
)
|
||||
|
||||
columns, data = execute(filters=filters)
|
||||
self.assertGreater(len(data), 0)
|
||||
|
||||
@@ -53,9 +53,6 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
|
||||
item_details = get_item_details()
|
||||
|
||||
for d in item_list:
|
||||
if not d.stock_qty:
|
||||
continue
|
||||
|
||||
item_record = item_details.get(d.item_code)
|
||||
|
||||
purchase_receipt = None
|
||||
@@ -94,7 +91,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
|
||||
"expense_account": expense_account,
|
||||
"stock_qty": d.stock_qty,
|
||||
"stock_uom": d.stock_uom,
|
||||
"rate": d.base_net_amount / d.stock_qty,
|
||||
"rate": d.base_net_amount / d.stock_qty if d.stock_qty else d.base_net_amount,
|
||||
"amount": d.base_net_amount,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -63,24 +63,6 @@ frappe.query_reports["Supplier Ledger Summary"] = {
|
||||
"fieldtype": "Link",
|
||||
"options": "Payment Terms Template"
|
||||
},
|
||||
{
|
||||
"fieldname":"territory",
|
||||
"label": __("Territory"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Territory"
|
||||
},
|
||||
{
|
||||
"fieldname":"sales_partner",
|
||||
"label": __("Sales Partner"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Sales Partner"
|
||||
},
|
||||
{
|
||||
"fieldname":"sales_person",
|
||||
"label": __("Sales Person"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Sales Person"
|
||||
},
|
||||
{
|
||||
"fieldname":"tax_id",
|
||||
"label": __("Tax Id"),
|
||||
|
||||
@@ -370,7 +370,7 @@
|
||||
{
|
||||
"fieldname": "shipping_address",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company Shipping Address",
|
||||
"label": "Shipping Address",
|
||||
"options": "Address",
|
||||
"print_hide": 1
|
||||
},
|
||||
@@ -1170,7 +1170,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-17 12:34:36.033363",
|
||||
"modified": "2022-12-25 18:08:59.074182",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
|
||||
@@ -573,7 +573,12 @@ class AccountsController(TransactionBase):
|
||||
if bool(uom) != bool(stock_uom): # xor
|
||||
item.stock_uom = item.uom = uom or stock_uom
|
||||
|
||||
item.conversion_factor = get_uom_conv_factor(item.get("uom"), item.get("stock_uom"))
|
||||
# UOM cannot be zero so substitute as 1
|
||||
item.conversion_factor = (
|
||||
get_uom_conv_factor(item.get("uom"), item.get("stock_uom"))
|
||||
or item.get("conversion_factor")
|
||||
or 1
|
||||
)
|
||||
|
||||
if self.doctype == "Purchase Invoice":
|
||||
self.set_expense_account(for_validate)
|
||||
|
||||
@@ -26,7 +26,7 @@ class SellingController(StockController):
|
||||
super(SellingController, self).onload()
|
||||
if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"):
|
||||
for item in self.get("items"):
|
||||
item.update(get_bin_details(item.item_code, item.warehouse))
|
||||
item.update(get_bin_details(item.item_code, item.warehouse, include_child_warehouses=True))
|
||||
|
||||
def validate(self):
|
||||
super(SellingController, self).validate()
|
||||
|
||||
@@ -333,16 +333,21 @@ class StatusUpdater(Document):
|
||||
)
|
||||
|
||||
def warn_about_bypassing_with_role(self, item, qty_or_amount, role):
|
||||
action = _("Over Receipt/Delivery") if qty_or_amount == "qty" else _("Overbilling")
|
||||
if qty_or_amount == "qty":
|
||||
msg = _("Over Receipt/Delivery of {0} {1} ignored for item {2} because you have {3} role.")
|
||||
else:
|
||||
msg = _("Overbilling of {0} {1} ignored for item {2} because you have {3} role.")
|
||||
|
||||
msg = _("{} of {} {} ignored for item {} because you have {} role.").format(
|
||||
action,
|
||||
_(item["target_ref_field"].title()),
|
||||
frappe.bold(item["reduce_by"]),
|
||||
frappe.bold(item.get("item_code")),
|
||||
role,
|
||||
frappe.msgprint(
|
||||
msg.format(
|
||||
_(item["target_ref_field"].title()),
|
||||
frappe.bold(item["reduce_by"]),
|
||||
frappe.bold(item.get("item_code")),
|
||||
role,
|
||||
),
|
||||
indicator="orange",
|
||||
alert=True,
|
||||
)
|
||||
frappe.msgprint(msg, indicator="orange", alert=True)
|
||||
|
||||
def update_qty(self, update_modified=True):
|
||||
"""Updates qty or amount at row level
|
||||
|
||||
@@ -5,7 +5,16 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, cstr, formatdate, get_datetime, getdate, nowdate
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
cint,
|
||||
cstr,
|
||||
formatdate,
|
||||
get_datetime,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
nowdate,
|
||||
)
|
||||
|
||||
from erpnext.hr.utils import get_holiday_dates_for_employee, validate_active_employee
|
||||
|
||||
@@ -106,8 +115,6 @@ class Attendance(Document):
|
||||
frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee))
|
||||
|
||||
def unlink_attendance_from_checkins(self):
|
||||
from frappe.utils import get_link_to_form
|
||||
|
||||
EmployeeCheckin = frappe.qb.DocType("Employee Checkin")
|
||||
linked_logs = (
|
||||
frappe.qb.from_(EmployeeCheckin)
|
||||
@@ -221,75 +228,39 @@ def mark_bulk_attendance(data):
|
||||
attendance.submit()
|
||||
|
||||
|
||||
def get_month_map():
|
||||
return frappe._dict(
|
||||
{
|
||||
"January": 1,
|
||||
"February": 2,
|
||||
"March": 3,
|
||||
"April": 4,
|
||||
"May": 5,
|
||||
"June": 6,
|
||||
"July": 7,
|
||||
"August": 8,
|
||||
"September": 9,
|
||||
"October": 10,
|
||||
"November": 11,
|
||||
"December": 12,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_unmarked_days(employee, month, exclude_holidays=0):
|
||||
import calendar
|
||||
|
||||
month_map = get_month_map()
|
||||
today = get_datetime()
|
||||
|
||||
def get_unmarked_days(employee, from_date, to_date, exclude_holidays=0):
|
||||
joining_date, relieving_date = frappe.get_cached_value(
|
||||
"Employee", employee, ["date_of_joining", "relieving_date"]
|
||||
)
|
||||
start_day = 1
|
||||
end_day = calendar.monthrange(today.year, month_map[month])[1] + 1
|
||||
|
||||
if joining_date and joining_date.year == today.year and joining_date.month == month_map[month]:
|
||||
start_day = joining_date.day
|
||||
|
||||
if (
|
||||
relieving_date and relieving_date.year == today.year and relieving_date.month == month_map[month]
|
||||
):
|
||||
end_day = relieving_date.day + 1
|
||||
|
||||
dates_of_month = [
|
||||
"{}-{}-{}".format(today.year, month_map[month], r) for r in range(start_day, end_day)
|
||||
]
|
||||
month_start, month_end = dates_of_month[0], dates_of_month[-1]
|
||||
from_date = max(getdate(from_date), joining_date or getdate(from_date))
|
||||
to_date = min(getdate(to_date), relieving_date or getdate(to_date))
|
||||
|
||||
records = frappe.get_all(
|
||||
"Attendance",
|
||||
fields=["attendance_date", "employee"],
|
||||
filters=[
|
||||
["attendance_date", ">=", month_start],
|
||||
["attendance_date", "<=", month_end],
|
||||
["attendance_date", ">=", from_date],
|
||||
["attendance_date", "<=", to_date],
|
||||
["employee", "=", employee],
|
||||
["docstatus", "!=", 2],
|
||||
],
|
||||
)
|
||||
|
||||
marked_days = [get_datetime(record.attendance_date) for record in records]
|
||||
marked_days = [getdate(record.attendance_date) for record in records]
|
||||
|
||||
if cint(exclude_holidays):
|
||||
holiday_dates = get_holiday_dates_for_employee(employee, month_start, month_end)
|
||||
holidays = [get_datetime(record) for record in holiday_dates]
|
||||
holiday_dates = get_holiday_dates_for_employee(employee, from_date, to_date)
|
||||
holidays = [getdate(record) for record in holiday_dates]
|
||||
marked_days.extend(holidays)
|
||||
|
||||
unmarked_days = []
|
||||
|
||||
for date in dates_of_month:
|
||||
date_time = get_datetime(date)
|
||||
if today.day <= date_time.day and today.month <= date_time.month:
|
||||
break
|
||||
if date_time not in marked_days:
|
||||
unmarked_days.append(date)
|
||||
while from_date <= to_date:
|
||||
if from_date not in marked_days:
|
||||
unmarked_days.append(from_date)
|
||||
|
||||
from_date = add_days(from_date, 1)
|
||||
|
||||
return unmarked_days
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
frappe.listview_settings['Attendance'] = {
|
||||
frappe.listview_settings["Attendance"] = {
|
||||
add_fields: ["status", "attendance_date"],
|
||||
|
||||
get_indicator: function (doc) {
|
||||
if (["Present", "Work From Home"].includes(doc.status)) {
|
||||
return [__(doc.status), "green", "status,=," + doc.status];
|
||||
@@ -10,157 +11,185 @@ frappe.listview_settings['Attendance'] = {
|
||||
}
|
||||
},
|
||||
|
||||
onload: function(list_view) {
|
||||
onload: function (list_view) {
|
||||
let me = this;
|
||||
const months = moment.months();
|
||||
const curMonth = moment().format("MMMM");
|
||||
months.splice(months.indexOf(curMonth) + 1);
|
||||
list_view.page.add_inner_button(__("Mark Attendance"), function() {
|
||||
|
||||
list_view.page.add_inner_button(__("Mark Attendance"), function () {
|
||||
let first_day_of_month = moment().startOf('month');
|
||||
|
||||
if (moment().toDate().getDate() === 1) {
|
||||
first_day_of_month = first_day_of_month.subtract(1, "month");
|
||||
}
|
||||
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __("Mark Attendance"),
|
||||
fields: [{
|
||||
fieldname: 'employee',
|
||||
label: __('For Employee'),
|
||||
fieldtype: 'Link',
|
||||
options: 'Employee',
|
||||
get_query: () => {
|
||||
return {query: "erpnext.controllers.queries.employee_query"};
|
||||
fields: [
|
||||
{
|
||||
fieldname: "employee",
|
||||
label: __("For Employee"),
|
||||
fieldtype: "Link",
|
||||
options: "Employee",
|
||||
get_query: () => {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.employee_query",
|
||||
};
|
||||
},
|
||||
reqd: 1,
|
||||
onchange: () => me.reset_dialog(dialog),
|
||||
},
|
||||
reqd: 1,
|
||||
onchange: function() {
|
||||
dialog.set_df_property("unmarked_days", "hidden", 1);
|
||||
dialog.set_df_property("status", "hidden", 1);
|
||||
dialog.set_df_property("exclude_holidays", "hidden", 1);
|
||||
dialog.set_df_property("month", "value", '');
|
||||
dialog.set_df_property("unmarked_days", "options", []);
|
||||
dialog.no_unmarked_days_left = false;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: __("For Month"),
|
||||
fieldtype: "Select",
|
||||
fieldname: "month",
|
||||
options: months,
|
||||
reqd: 1,
|
||||
onchange: function() {
|
||||
if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
|
||||
dialog.set_df_property("status", "hidden", 0);
|
||||
dialog.set_df_property("exclude_holidays", "hidden", 0);
|
||||
dialog.set_df_property("unmarked_days", "options", []);
|
||||
dialog.no_unmarked_days_left = false;
|
||||
me.get_multi_select_options(
|
||||
dialog.fields_dict.employee.value,
|
||||
dialog.fields_dict.month.value,
|
||||
dialog.fields_dict.exclude_holidays.get_value()
|
||||
).then(options => {
|
||||
if (options.length > 0) {
|
||||
dialog.set_df_property("unmarked_days", "hidden", 0);
|
||||
dialog.set_df_property("unmarked_days", "options", options);
|
||||
} else {
|
||||
dialog.no_unmarked_days_left = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: __("Status"),
|
||||
fieldtype: "Select",
|
||||
fieldname: "status",
|
||||
options: ["Present", "Absent", "Half Day", "Work From Home"],
|
||||
hidden: 1,
|
||||
reqd: 1,
|
||||
|
||||
},
|
||||
{
|
||||
label: __("Exclude Holidays"),
|
||||
fieldtype: "Check",
|
||||
fieldname: "exclude_holidays",
|
||||
hidden: 1,
|
||||
onchange: function() {
|
||||
if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
|
||||
dialog.set_df_property("status", "hidden", 0);
|
||||
dialog.set_df_property("unmarked_days", "options", []);
|
||||
dialog.no_unmarked_days_left = false;
|
||||
me.get_multi_select_options(
|
||||
dialog.fields_dict.employee.value,
|
||||
dialog.fields_dict.month.value,
|
||||
dialog.fields_dict.exclude_holidays.get_value()
|
||||
).then(options => {
|
||||
if (options.length > 0) {
|
||||
dialog.set_df_property("unmarked_days", "hidden", 0);
|
||||
dialog.set_df_property("unmarked_days", "options", options);
|
||||
} else {
|
||||
dialog.no_unmarked_days_left = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: __("Unmarked Attendance for days"),
|
||||
fieldname: "unmarked_days",
|
||||
fieldtype: "MultiCheck",
|
||||
options: [],
|
||||
columns: 2,
|
||||
hidden: 1
|
||||
}],
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
fieldname: "time_period_section",
|
||||
hidden: 1,
|
||||
},
|
||||
{
|
||||
label: __("Start"),
|
||||
fieldtype: "Date",
|
||||
fieldname: "from_date",
|
||||
reqd: 1,
|
||||
default: first_day_of_month.toDate(),
|
||||
onchange: () => me.get_unmarked_days(dialog),
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
fieldname: "time_period_column",
|
||||
},
|
||||
{
|
||||
label: __("End"),
|
||||
fieldtype: "Date",
|
||||
fieldname: "to_date",
|
||||
reqd: 1,
|
||||
default: moment().subtract(1, 'days').toDate(),
|
||||
onchange: () => me.get_unmarked_days(dialog),
|
||||
},
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
fieldname: "days_section",
|
||||
hidden: 1,
|
||||
},
|
||||
{
|
||||
label: __("Status"),
|
||||
fieldtype: "Select",
|
||||
fieldname: "status",
|
||||
options: ["Present", "Absent", "Half Day", "Work From Home"],
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
label: __("Exclude Holidays"),
|
||||
fieldtype: "Check",
|
||||
fieldname: "exclude_holidays",
|
||||
onchange: () => me.get_unmarked_days(dialog),
|
||||
},
|
||||
{
|
||||
label: __("Unmarked Attendance for days"),
|
||||
fieldname: "unmarked_days",
|
||||
fieldtype: "MultiCheck",
|
||||
options: [],
|
||||
columns: 2,
|
||||
},
|
||||
],
|
||||
primary_action(data) {
|
||||
if (cur_dialog.no_unmarked_days_left) {
|
||||
frappe.msgprint(__("Attendance for the month of {0} , has already been marked for the Employee {1}",
|
||||
[dialog.fields_dict.month.value, dialog.fields_dict.employee.value]));
|
||||
frappe.msgprint(
|
||||
__(
|
||||
"Attendance from {0} to {1} has already been marked for the Employee {2}",
|
||||
[data.from_date, data.to_date, data.employee]
|
||||
)
|
||||
);
|
||||
} else {
|
||||
frappe.confirm(__('Mark attendance as {0} for {1} on selected dates?', [data.status, data.month]), () => {
|
||||
frappe.call({
|
||||
method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance",
|
||||
args: {
|
||||
data: data
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.message === 1) {
|
||||
frappe.show_alert({
|
||||
message: __("Attendance Marked"),
|
||||
indicator: 'blue'
|
||||
});
|
||||
cur_dialog.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
frappe.confirm(
|
||||
__("Mark attendance as {0} for {1} on selected dates?", [
|
||||
data.status,
|
||||
data.employee,
|
||||
]),
|
||||
() => {
|
||||
frappe.call({
|
||||
method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance",
|
||||
args: {
|
||||
data: data,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.message === 1) {
|
||||
frappe.show_alert({
|
||||
message: __("Attendance Marked"),
|
||||
indicator: "blue",
|
||||
});
|
||||
cur_dialog.hide();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
dialog.hide();
|
||||
list_view.refresh();
|
||||
},
|
||||
primary_action_label: __('Mark Attendance')
|
||||
|
||||
primary_action_label: __("Mark Attendance"),
|
||||
});
|
||||
dialog.show();
|
||||
});
|
||||
},
|
||||
|
||||
get_multi_select_options: function(employee, month, exclude_holidays) {
|
||||
return new Promise(resolve => {
|
||||
frappe.call({
|
||||
method: 'erpnext.hr.doctype.attendance.attendance.get_unmarked_days',
|
||||
async: false,
|
||||
args: {
|
||||
employee: employee,
|
||||
month: month,
|
||||
exclude_holidays: exclude_holidays
|
||||
}
|
||||
}).then(r => {
|
||||
var options = [];
|
||||
for (var d in r.message) {
|
||||
var momentObj = moment(r.message[d], 'YYYY-MM-DD');
|
||||
var date = momentObj.format('DD-MM-YYYY');
|
||||
options.push({
|
||||
"label": date,
|
||||
"value": r.message[d],
|
||||
"checked": 1
|
||||
});
|
||||
}
|
||||
resolve(options);
|
||||
});
|
||||
reset_dialog: function (dialog) {
|
||||
let fields = dialog.fields_dict;
|
||||
|
||||
dialog.set_df_property(
|
||||
"time_period_section",
|
||||
"hidden",
|
||||
fields.employee.value ? 0 : 1
|
||||
);
|
||||
|
||||
dialog.set_df_property("days_section", "hidden", 1);
|
||||
dialog.set_df_property("unmarked_days", "options", []);
|
||||
dialog.no_unmarked_days_left = false;
|
||||
fields.exclude_holidays.value = false;
|
||||
|
||||
fields.to_date.datepicker.update({
|
||||
maxDate: moment().subtract(1, 'days').toDate()
|
||||
});
|
||||
}
|
||||
|
||||
this.get_unmarked_days(dialog)
|
||||
},
|
||||
|
||||
get_unmarked_days: function (dialog) {
|
||||
let fields = dialog.fields_dict;
|
||||
if (fields.employee.value && fields.from_date.value && fields.to_date.value) {
|
||||
dialog.set_df_property("days_section", "hidden", 0);
|
||||
dialog.set_df_property("status", "hidden", 0);
|
||||
dialog.set_df_property("exclude_holidays", "hidden", 0);
|
||||
dialog.no_unmarked_days_left = false;
|
||||
|
||||
frappe
|
||||
.call({
|
||||
method: "erpnext.hr.doctype.attendance.attendance.get_unmarked_days",
|
||||
async: false,
|
||||
args: {
|
||||
employee: fields.employee.value,
|
||||
from_date: fields.from_date.value,
|
||||
to_date: fields.to_date.value,
|
||||
exclude_holidays: fields.exclude_holidays.value,
|
||||
},
|
||||
})
|
||||
.then((r) => {
|
||||
var options = [];
|
||||
|
||||
for (var d in r.message) {
|
||||
var momentObj = moment(r.message[d], "YYYY-MM-DD");
|
||||
var date = momentObj.format("DD-MM-YYYY");
|
||||
options.push({
|
||||
label: date,
|
||||
value: r.message[d],
|
||||
checked: 1,
|
||||
});
|
||||
}
|
||||
|
||||
dialog.set_df_property(
|
||||
"unmarked_days",
|
||||
"options",
|
||||
options.length > 0 ? options : []
|
||||
);
|
||||
dialog.no_unmarked_days_left = options.length === 0;
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
get_first_day,
|
||||
get_last_day,
|
||||
get_year_ending,
|
||||
get_year_start,
|
||||
@@ -13,11 +14,7 @@ from frappe.utils import (
|
||||
nowdate,
|
||||
)
|
||||
|
||||
from erpnext.hr.doctype.attendance.attendance import (
|
||||
get_month_map,
|
||||
get_unmarked_days,
|
||||
mark_attendance,
|
||||
)
|
||||
from erpnext.hr.doctype.attendance.attendance import get_unmarked_days, mark_attendance
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.hr.tests.test_utils import get_first_sunday
|
||||
|
||||
@@ -28,7 +25,7 @@ class TestAttendance(FrappeTestCase):
|
||||
def setUp(self):
|
||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
|
||||
|
||||
from_date = get_year_start(getdate())
|
||||
from_date = get_year_start(add_months(getdate(), -1))
|
||||
to_date = get_year_ending(getdate())
|
||||
self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date)
|
||||
|
||||
@@ -55,9 +52,10 @@ class TestAttendance(FrappeTestCase):
|
||||
frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
|
||||
|
||||
mark_attendance(employee, attendance_date, "Present")
|
||||
month_name = get_month_name(attendance_date)
|
||||
|
||||
unmarked_days = get_unmarked_days(employee, month_name)
|
||||
unmarked_days = get_unmarked_days(
|
||||
employee, get_first_day(attendance_date), get_last_day(attendance_date)
|
||||
)
|
||||
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||
|
||||
# attendance already marked for the day
|
||||
@@ -81,9 +79,10 @@ class TestAttendance(FrappeTestCase):
|
||||
frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
|
||||
|
||||
mark_attendance(employee, attendance_date, "Present")
|
||||
month_name = get_month_name(attendance_date)
|
||||
|
||||
unmarked_days = get_unmarked_days(employee, month_name, exclude_holidays=True)
|
||||
unmarked_days = unmarked_days = get_unmarked_days(
|
||||
employee, get_first_day(attendance_date), get_last_day(attendance_date), exclude_holidays=True
|
||||
)
|
||||
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||
|
||||
# attendance already marked for the day
|
||||
@@ -110,9 +109,10 @@ class TestAttendance(FrappeTestCase):
|
||||
|
||||
attendance_date = add_days(date, 2)
|
||||
mark_attendance(employee, attendance_date, "Present")
|
||||
month_name = get_month_name(attendance_date)
|
||||
|
||||
unmarked_days = get_unmarked_days(employee, month_name)
|
||||
unmarked_days = get_unmarked_days(
|
||||
employee, get_first_day(attendance_date), get_last_day(attendance_date)
|
||||
)
|
||||
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||
|
||||
# attendance already marked for the day
|
||||
@@ -124,10 +124,3 @@ class TestAttendance(FrappeTestCase):
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
|
||||
def get_month_name(date):
|
||||
month_number = date.month
|
||||
for month, number in get_month_map().items():
|
||||
if number == month_number:
|
||||
return month
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import frappe
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import now_datetime
|
||||
from frappe.utils import add_months, getdate
|
||||
|
||||
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
@@ -14,9 +14,7 @@ class TestMonthlyAttendanceSheet(FrappeTestCase):
|
||||
frappe.db.delete("Attendance", {"employee": self.employee})
|
||||
|
||||
def test_monthly_attendance_sheet_report(self):
|
||||
now = now_datetime()
|
||||
previous_month = now.month - 1
|
||||
previous_month_first = now.replace(day=1).replace(month=previous_month).date()
|
||||
previous_month_first = add_months(getdate(), -1).replace(day=1)
|
||||
|
||||
company = frappe.db.get_value("Employee", self.employee, "company")
|
||||
|
||||
@@ -27,8 +25,8 @@ class TestMonthlyAttendanceSheet(FrappeTestCase):
|
||||
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"month": previous_month,
|
||||
"year": now.year,
|
||||
"month": previous_month_first.month,
|
||||
"year": previous_month_first.year,
|
||||
"company": company,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1145,6 +1145,37 @@ class TestWorkOrder(FrappeTestCase):
|
||||
except frappe.MandatoryError:
|
||||
self.fail("Batch generation causing failing in Work Order")
|
||||
|
||||
@change_settings("Manufacturing Settings", {"make_serial_no_batch_from_work_order": 1})
|
||||
def test_auto_serial_no_creation(self):
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
fg_item = frappe.generate_hash(length=20)
|
||||
child_item = frappe.generate_hash(length=20)
|
||||
|
||||
bom_tree = {fg_item: {child_item: {}}}
|
||||
|
||||
create_nested_bom(bom_tree, prefix="")
|
||||
|
||||
item = frappe.get_doc("Item", fg_item)
|
||||
item.has_serial_no = 1
|
||||
item.serial_no_series = f"{item.name}.#####"
|
||||
item.save()
|
||||
|
||||
try:
|
||||
wo_order = make_wo_order_test_record(item=fg_item, qty=2, skip_transfer=True)
|
||||
serial_nos = wo_order.serial_no
|
||||
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
|
||||
stock_entry.set_work_order_details()
|
||||
stock_entry.set_serial_no_batch_for_finished_good()
|
||||
for row in stock_entry.items:
|
||||
if row.item_code == fg_item:
|
||||
self.assertTrue(row.serial_no)
|
||||
self.assertEqual(sorted(get_serial_nos(row.serial_no)), sorted(get_serial_nos(serial_nos)))
|
||||
|
||||
except frappe.MandatoryError:
|
||||
self.fail("Batch generation causing failing in Work Order")
|
||||
|
||||
@change_settings(
|
||||
"Manufacturing Settings",
|
||||
{"backflush_raw_materials_based_on": "Material Transferred for Manufacture"},
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Floor, Sum
|
||||
from frappe.query_builder.functions import Sum
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
|
||||
@@ -58,9 +58,9 @@ def get_bom_stock(filters):
|
||||
bom_item.description,
|
||||
bom_item.stock_qty,
|
||||
bom_item.stock_uom,
|
||||
bom_item.stock_qty * qty_to_produce / bom.quantity,
|
||||
Sum(bin.actual_qty).as_("actual_qty"),
|
||||
Sum(Floor(bin.actual_qty / (bom_item.stock_qty * qty_to_produce / bom.quantity))),
|
||||
(bom_item.stock_qty / bom.quantity) * qty_to_produce,
|
||||
Sum(bin.actual_qty),
|
||||
Sum(bin.actual_qty) / (bom_item.stock_qty / bom.quantity),
|
||||
)
|
||||
.where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM"))
|
||||
.groupby(bom_item.item_code)
|
||||
|
||||
@@ -49,7 +49,7 @@ class ProductionPlanReport(object):
|
||||
parent.bom_no,
|
||||
parent.fg_warehouse.as_("warehouse"),
|
||||
)
|
||||
.where(parent.status.notin(["Completed", "Stopped"]))
|
||||
.where(parent.status.notin(["Completed", "Stopped", "Closed"]))
|
||||
)
|
||||
|
||||
if order_by == "Planned Start Date":
|
||||
@@ -79,10 +79,11 @@ class ProductionPlanReport(object):
|
||||
query = query.where(child.parent.isin(self.filters.docnames))
|
||||
|
||||
if doctype == "Sales Order":
|
||||
query = query.select(
|
||||
child.delivery_date,
|
||||
parent.base_grand_total,
|
||||
).where((child.stock_qty > child.produced_qty) & (parent.per_delivered < 100.0))
|
||||
query = query.select(child.delivery_date, parent.base_grand_total,).where(
|
||||
(child.stock_qty > child.produced_qty)
|
||||
& (parent.per_delivered < 100.0)
|
||||
& (parent.status.notin(["Completed", "Closed"]))
|
||||
)
|
||||
|
||||
if order_by == "Delivery Date":
|
||||
query = query.orderby(child.delivery_date, order=Order.asc)
|
||||
@@ -91,7 +92,9 @@ class ProductionPlanReport(object):
|
||||
|
||||
elif doctype == "Material Request":
|
||||
query = query.select(child.schedule_date,).where(
|
||||
(parent.per_ordered < 100) & (parent.material_request_type == "Manufacture")
|
||||
(parent.per_ordered < 100)
|
||||
& (parent.material_request_type == "Manufacture")
|
||||
& (parent.status != "Stopped")
|
||||
)
|
||||
|
||||
if order_by == "Required Date":
|
||||
|
||||
@@ -13,8 +13,10 @@ from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assign
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("Payroll", "doctype", "Payroll Settings")
|
||||
frappe.reload_doc("Payroll", "doctype", "Salary Structure")
|
||||
frappe.reload_doc("Payroll", "doctype", "Salary Structure Assignment")
|
||||
|
||||
frappe.db.sql(
|
||||
"""
|
||||
delete from `tabSalary Structure Assignment`
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"max_working_hours_against_timesheet",
|
||||
"include_holidays_in_total_working_days",
|
||||
"disable_rounded_total",
|
||||
"define_opening_balance_for_earning_and_deductions",
|
||||
"column_break_11",
|
||||
"daily_wages_fraction_for_half_day",
|
||||
"email_salary_slip_to_employee",
|
||||
@@ -91,13 +92,20 @@
|
||||
"fieldname": "show_leave_balances_in_salary_slip",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Leave Balances in Salary Slip"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If checked, then the system will enable the provision to set the opening balance for earnings and deductions till date while creating a Salary Structure Assignment (if any)",
|
||||
"fieldname": "define_opening_balance_for_earning_and_deductions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Define Opening Balance for Earning and Deductions"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-03 17:49:59.579723",
|
||||
"modified": "2022-12-21 17:30:08.704247",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Payroll",
|
||||
"name": "Payroll Settings",
|
||||
|
||||
@@ -1063,7 +1063,25 @@ class SalarySlip(TransactionBase):
|
||||
)
|
||||
exempted_amount = flt(exempted_amount[0][0]) if exempted_amount else 0
|
||||
|
||||
return taxable_earnings - exempted_amount
|
||||
opening_taxable_earning = self.get_opening_balance_for(
|
||||
"taxable_earnings_till_date", start_date, end_date
|
||||
)
|
||||
|
||||
return (taxable_earnings + opening_taxable_earning) - exempted_amount
|
||||
|
||||
def get_opening_balance_for(self, field_to_select, start_date, end_date):
|
||||
opening_balance = frappe.db.get_all(
|
||||
"Salary Structure Assignment",
|
||||
{
|
||||
"employee": self.employee,
|
||||
"salary_structure": self.salary_structure,
|
||||
"from_date": ["between", (start_date, end_date)],
|
||||
"docstatus": 1,
|
||||
},
|
||||
field_to_select,
|
||||
)
|
||||
|
||||
return opening_balance[0].get(field_to_select) if opening_balance else 0.0
|
||||
|
||||
def get_tax_paid_in_period(self, start_date, end_date, tax_component):
|
||||
# find total_tax_paid, tax paid for benefit, additional_salary
|
||||
@@ -1092,7 +1110,11 @@ class SalarySlip(TransactionBase):
|
||||
)[0][0]
|
||||
)
|
||||
|
||||
return total_tax_paid
|
||||
tax_deducted_till_date = self.get_opening_balance_for(
|
||||
"tax_deducted_till_date", start_date, end_date
|
||||
)
|
||||
|
||||
return total_tax_paid + tax_deducted_till_date
|
||||
|
||||
def get_taxable_earnings(
|
||||
self, allow_tax_exemption=False, based_on_payment_days=0, payroll_period=None
|
||||
|
||||
@@ -1030,6 +1030,104 @@ class TestSalarySlip(FrappeTestCase):
|
||||
activity_type.wage_rate = 25
|
||||
activity_type.save()
|
||||
|
||||
def test_salary_slip_generation_against_opening_entries_in_ssa(self):
|
||||
import math
|
||||
|
||||
from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_factor
|
||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||
|
||||
payroll_period = frappe.db.get_value(
|
||||
"Payroll Period",
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"start_date": ["<=", "2023-03-31"],
|
||||
"end_date": [">=", "2022-04-01"],
|
||||
},
|
||||
"name",
|
||||
)
|
||||
|
||||
if not payroll_period:
|
||||
payroll_period = create_payroll_period(
|
||||
name="_Test Payroll Period for Tax",
|
||||
company="_Test Company",
|
||||
start_date="2022-04-01",
|
||||
end_date="2023-03-31",
|
||||
)
|
||||
else:
|
||||
payroll_period = frappe.get_cached_doc("Payroll Period", payroll_period)
|
||||
|
||||
emp = make_employee(
|
||||
"test_employee_ss_with_opening_balance@salary.com",
|
||||
company="_Test Company",
|
||||
**{"date_of_joining": "2021-12-01"},
|
||||
)
|
||||
employee_doc = frappe.get_doc("Employee", emp)
|
||||
|
||||
create_tax_slab(payroll_period, allow_tax_exemption=True)
|
||||
|
||||
salary_structure_name = "Test Salary Structure for Opening Balance"
|
||||
if not frappe.db.exists("Salary Structure", salary_structure_name):
|
||||
salary_structure_doc = make_salary_structure(
|
||||
salary_structure_name,
|
||||
"Monthly",
|
||||
company="_Test Company",
|
||||
employee=emp,
|
||||
from_date="2022-04-01",
|
||||
payroll_period=payroll_period,
|
||||
test_tax=True,
|
||||
)
|
||||
|
||||
# validate no salary slip exists for the employee
|
||||
self.assertTrue(
|
||||
frappe.db.count(
|
||||
"Salary Slip",
|
||||
{
|
||||
"employee": emp,
|
||||
"salary_structure": salary_structure_doc.name,
|
||||
"docstatus": 1,
|
||||
"start_date": [">=", "2022-04-01"],
|
||||
},
|
||||
)
|
||||
== 0
|
||||
)
|
||||
|
||||
remaining_sub_periods = get_period_factor(
|
||||
emp,
|
||||
get_first_day("2022-10-01"),
|
||||
get_last_day("2022-10-01"),
|
||||
"Monthly",
|
||||
payroll_period,
|
||||
depends_on_payment_days=0,
|
||||
)[1]
|
||||
|
||||
prev_period = math.ceil(remaining_sub_periods)
|
||||
|
||||
annual_tax = 93036 # 89220 #data[0].get('applicable_tax')
|
||||
monthly_tax_amount = 7732.40 # 7435 #annual_tax/12
|
||||
annual_earnings = 933600 # data[0].get('ctc')
|
||||
monthly_earnings = 77800 # annual_earnings/12
|
||||
|
||||
# Get Salary Structure Assignment
|
||||
ssa = frappe.get_value(
|
||||
"Salary Structure Assignment",
|
||||
{"employee": emp, "salary_structure": salary_structure_doc.name},
|
||||
"name",
|
||||
)
|
||||
ssa_doc = frappe.get_doc("Salary Structure Assignment", ssa)
|
||||
|
||||
# Set opening balance for earning and tax deduction in Salary Structure Assignment
|
||||
ssa_doc.taxable_earnings_till_date = monthly_earnings * prev_period
|
||||
ssa_doc.tax_deducted_till_date = monthly_tax_amount * prev_period
|
||||
ssa_doc.save()
|
||||
|
||||
# Create Salary Slip
|
||||
salary_slip = make_salary_slip(
|
||||
salary_structure_doc.name, employee=employee_doc.name, posting_date=getdate("2022-10-01")
|
||||
)
|
||||
for deduction in salary_slip.deductions:
|
||||
if deduction.salary_component == "TDS":
|
||||
self.assertEqual(deduction.amount, rounded(monthly_tax_amount))
|
||||
|
||||
|
||||
def get_no_of_days():
|
||||
no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month)
|
||||
|
||||
@@ -42,6 +42,13 @@ frappe.ui.form.on('Salary Structure Assignment', {
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
if(frm.doc.__onload){
|
||||
frm.unhide_earnings_and_taxation_section = frm.doc.__onload.earning_and_deduction_entries_does_not_exists;
|
||||
frm.trigger("set_earnings_and_taxation_section_visibility");
|
||||
}
|
||||
},
|
||||
|
||||
employee: function(frm) {
|
||||
if(frm.doc.employee){
|
||||
frappe.call({
|
||||
@@ -59,6 +66,8 @@ frappe.ui.form.on('Salary Structure Assignment', {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
frm.trigger("valiadte_joining_date_and_salary_slips");
|
||||
}
|
||||
else{
|
||||
frm.set_value("company", null);
|
||||
@@ -71,5 +80,33 @@ frappe.ui.form.on('Salary Structure Assignment', {
|
||||
frm.set_value("payroll_payable_account", r.default_payroll_payable_account);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
valiadte_joining_date_and_salary_slips: function(frm) {
|
||||
frappe.call({
|
||||
method: "earning_and_deduction_entries_does_not_exists",
|
||||
doc: frm.doc,
|
||||
callback: function(data) {
|
||||
let earning_and_deduction_entries_does_not_exists = data.message;
|
||||
frm.unhide_earnings_and_taxation_section = earning_and_deduction_entries_does_not_exists;
|
||||
frm.trigger("set_earnings_and_taxation_section_visibility");
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
set_earnings_and_taxation_section_visibility: function(frm) {
|
||||
if(frm.unhide_earnings_and_taxation_section){
|
||||
frm.set_df_property('earnings_and_taxation_section', 'hidden', 0);
|
||||
}
|
||||
else{
|
||||
frm.set_df_property('earnings_and_taxation_section', 'hidden', 1);
|
||||
}
|
||||
},
|
||||
|
||||
from_date: function(frm) {
|
||||
if (frm.doc.from_date) {
|
||||
frm.trigger("valiadte_joining_date_and_salary_slips" );
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
"base",
|
||||
"column_break_9",
|
||||
"variable",
|
||||
"earnings_and_taxation_section",
|
||||
"taxable_earnings_till_date",
|
||||
"column_break_18",
|
||||
"tax_deducted_till_date",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
@@ -141,11 +145,31 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Payroll Payable Account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "earnings_and_taxation_section",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "tax_deducted_till_date",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Tax Deducted Till Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_18",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "taxable_earnings_till_date",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Taxable Earnings Till Date"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-31 22:44:46.267974",
|
||||
"modified": "2022-12-26 12:47:42.521891",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Payroll",
|
||||
"name": "Salary Structure Assignment",
|
||||
|
||||
@@ -13,10 +13,32 @@ class DuplicateAssignment(frappe.ValidationError):
|
||||
|
||||
|
||||
class SalaryStructureAssignment(Document):
|
||||
def onload(self):
|
||||
if self.employee:
|
||||
self.set_onload(
|
||||
"earning_and_deduction_entries_exists", self.earning_and_deduction_entries_does_not_exists()
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
self.validate_dates()
|
||||
self.validate_income_tax_slab()
|
||||
self.set_payroll_payable_account()
|
||||
self.valiadte_missing_taxable_earnings_and_deductions_till_date()
|
||||
|
||||
def valiadte_missing_taxable_earnings_and_deductions_till_date(self):
|
||||
if self.earning_and_deduction_entries_does_not_exists():
|
||||
if not self.taxable_earnings_till_date and not self.tax_deducted_till_date:
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"""Not found any salary slip record(s) for the employee {0}.<br><br>Please specify {1} and {2} (if any), for the correct tax calculation in future salary slips."""
|
||||
).format(
|
||||
self.employee,
|
||||
"<b>" + _("Taxable Earnings Till Date") + "</b>",
|
||||
"<b>" + _("Tax Deducted Till Date") + "</b>",
|
||||
),
|
||||
indicator="orange",
|
||||
title=_("Warning"),
|
||||
)
|
||||
|
||||
def validate_dates(self):
|
||||
joining_date, relieving_date = frappe.db.get_value(
|
||||
@@ -76,6 +98,56 @@ class SalaryStructureAssignment(Document):
|
||||
)
|
||||
self.payroll_payable_account = payroll_payable_account
|
||||
|
||||
@frappe.whitelist()
|
||||
def earning_and_deduction_entries_does_not_exists(self):
|
||||
if self.enabled_settings_to_specify_earnings_and_deductions_till_date():
|
||||
if not self.joined_in_the_same_month() and not self.have_salary_slips():
|
||||
return True
|
||||
else:
|
||||
if self.docstatus in [1, 2] and (
|
||||
self.taxable_earnings_till_date or self.tax_deducted_till_date
|
||||
):
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
def enabled_settings_to_specify_earnings_and_deductions_till_date(self):
|
||||
"""returns True if settings are enabled to specify earnings and deductions till date else False"""
|
||||
|
||||
if frappe.db.get_single_value(
|
||||
"Payroll Settings", "define_opening_balance_for_earning_and_deductions"
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
def have_salary_slips(self):
|
||||
"""returns True if salary structure assignment has salary slips else False"""
|
||||
|
||||
salary_slip = frappe.db.get_value(
|
||||
"Salary Slip", filters={"employee": self.employee, "docstatus": 1}
|
||||
)
|
||||
|
||||
if salary_slip:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def joined_in_the_same_month(self):
|
||||
"""returns True if employee joined in same month as salary structure assignment from date else False"""
|
||||
|
||||
date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining")
|
||||
from_date = getdate(self.from_date)
|
||||
|
||||
if not self.from_date or not date_of_joining:
|
||||
return False
|
||||
|
||||
elif date_of_joining.month == from_date.month:
|
||||
return True
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def get_assigned_salary_structure(employee, on_date):
|
||||
if not employee or not on_date:
|
||||
|
||||
@@ -371,12 +371,14 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
fieldname: "deposit",
|
||||
fieldtype: "Currency",
|
||||
label: "Deposit",
|
||||
options: "currency",
|
||||
read_only: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "withdrawal",
|
||||
fieldtype: "Currency",
|
||||
label: "Withdrawal",
|
||||
options: "currency",
|
||||
read_only: 1,
|
||||
},
|
||||
{
|
||||
@@ -394,6 +396,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
fieldname: "allocated_amount",
|
||||
fieldtype: "Currency",
|
||||
label: "Allocated Amount",
|
||||
options: "Currency",
|
||||
read_only: 1,
|
||||
},
|
||||
|
||||
@@ -401,8 +404,17 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
fieldname: "unallocated_amount",
|
||||
fieldtype: "Currency",
|
||||
label: "Unallocated Amount",
|
||||
options: "Currency",
|
||||
read_only: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "currency",
|
||||
fieldtype: "Link",
|
||||
label: "Currency",
|
||||
options: "Currency",
|
||||
read_only: 1,
|
||||
hidden: 1,
|
||||
}
|
||||
];
|
||||
|
||||
me.dialog = new frappe.ui.Dialog({
|
||||
|
||||
@@ -217,7 +217,8 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
|
||||
args: {
|
||||
item_code: item.item_code,
|
||||
warehouse: item.warehouse,
|
||||
company: doc.company
|
||||
company: doc.company,
|
||||
include_child_warehouses: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1090,6 +1090,15 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
||||
]
|
||||
items_to_map = list(set(items_to_map))
|
||||
|
||||
def is_drop_ship_order(target):
|
||||
drop_ship = True
|
||||
for item in target.items:
|
||||
if not item.delivered_by_supplier:
|
||||
drop_ship = False
|
||||
break
|
||||
|
||||
return drop_ship
|
||||
|
||||
def set_missing_values(source, target):
|
||||
target.supplier = ""
|
||||
target.apply_discount_on = ""
|
||||
@@ -1097,8 +1106,14 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
||||
target.discount_amount = 0.0
|
||||
target.inter_company_order_reference = ""
|
||||
target.shipping_rule = ""
|
||||
target.customer = ""
|
||||
target.customer_name = ""
|
||||
|
||||
if is_drop_ship_order(target):
|
||||
target.customer = source.customer
|
||||
target.customer_name = source.customer_name
|
||||
target.shipping_address = source.shipping_address_name
|
||||
else:
|
||||
target.customer = target.customer_name = target.shipping_address = None
|
||||
|
||||
target.run_method("set_missing_values")
|
||||
target.run_method("calculate_taxes_and_totals")
|
||||
|
||||
|
||||
@@ -12,7 +12,10 @@ def get_data():
|
||||
"Auto Repeat": "reference_document",
|
||||
"Maintenance Visit": "prevdoc_docname",
|
||||
},
|
||||
"internal_links": {"Quotation": ["items", "prevdoc_docname"]},
|
||||
"internal_links": {
|
||||
"Quotation": ["items", "prevdoc_docname"],
|
||||
"Material Request": ["items", "material_request"],
|
||||
},
|
||||
"transactions": [
|
||||
{
|
||||
"label": _("Fulfillment"),
|
||||
|
||||
@@ -139,10 +139,11 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-08 17:01:52.162202",
|
||||
"modified": "2022-12-24 11:15:17.142746",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Customer Group",
|
||||
"naming_rule": "By fieldname",
|
||||
"nsm_parent_field": "parent_customer_group",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
@@ -198,10 +199,19 @@
|
||||
"role": "Customer",
|
||||
"select": 1,
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "parent_customer_group",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -153,7 +153,7 @@ def get_parent_item_groups(item_group_name, from_item=False):
|
||||
|
||||
if from_item and frappe.request.environ.get("HTTP_REFERER"):
|
||||
# base page after 'Home' will vary on Item page
|
||||
last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1]
|
||||
last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1].split("?")[0]
|
||||
if last_page and last_page in ("shop-by-category", "all-products"):
|
||||
base_nav_page_title = " ".join(last_page.split("-")).title()
|
||||
base_nav_page = {"name": _(base_nav_page_title), "route": "/" + last_page}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"creation": "2013-01-10 16:34:24",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"supplier_group_name",
|
||||
"parent_supplier_group",
|
||||
@@ -106,10 +107,11 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2020-03-18 18:10:49.228407",
|
||||
"modified": "2022-12-24 11:16:12.486719",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Supplier Group",
|
||||
"naming_rule": "By fieldname",
|
||||
"nsm_parent_field": "parent_supplier_group",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
@@ -156,8 +158,18 @@
|
||||
"permlevel": 1,
|
||||
"read": 1,
|
||||
"role": "Purchase User"
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_order": "ASC"
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
@@ -123,11 +123,12 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-08 17:10:03.767426",
|
||||
"modified": "2022-12-24 11:16:39.964956",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Territory",
|
||||
"name_case": "Title Case",
|
||||
"naming_rule": "By fieldname",
|
||||
"nsm_parent_field": "parent_territory",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
@@ -175,10 +176,19 @@
|
||||
"role": "Customer",
|
||||
"select": 1,
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "parent_territory,territory_manager",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -81,6 +81,7 @@ class TestItem(FrappeTestCase):
|
||||
def test_get_item_details(self):
|
||||
# delete modified item price record and make as per test_records
|
||||
frappe.db.sql("""delete from `tabItem Price`""")
|
||||
frappe.db.sql("""delete from `tabBin`""")
|
||||
|
||||
to_check = {
|
||||
"item_code": "_Test Item",
|
||||
@@ -101,9 +102,26 @@ class TestItem(FrappeTestCase):
|
||||
"batch_no": None,
|
||||
"uom": "_Test UOM",
|
||||
"conversion_factor": 1.0,
|
||||
"reserved_qty": 1,
|
||||
"actual_qty": 5,
|
||||
"ordered_qty": 10,
|
||||
"projected_qty": 14,
|
||||
}
|
||||
|
||||
make_test_objects("Item Price")
|
||||
make_test_objects(
|
||||
"Bin",
|
||||
[
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"reserved_qty": 1,
|
||||
"actual_qty": 5,
|
||||
"ordered_qty": 10,
|
||||
"projected_qty": 14,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
company = "_Test Company"
|
||||
currency = frappe.get_cached_value("Company", company, "default_currency")
|
||||
@@ -127,7 +145,7 @@ class TestItem(FrappeTestCase):
|
||||
)
|
||||
|
||||
for key, value in to_check.items():
|
||||
self.assertEqual(value, details.get(key))
|
||||
self.assertEqual(value, details.get(key), key)
|
||||
|
||||
def test_item_tax_template(self):
|
||||
expected_item_tax_template = [
|
||||
|
||||
@@ -181,13 +181,13 @@ class PickList(Document):
|
||||
|
||||
if item_map.get(key):
|
||||
item_map[key].qty += item.qty
|
||||
item_map[key].stock_qty += item.stock_qty
|
||||
item_map[key].stock_qty += flt(item.stock_qty, item.precision("stock_qty"))
|
||||
else:
|
||||
item_map[key] = item
|
||||
|
||||
# maintain count of each item (useful to limit get query)
|
||||
self.item_count_map.setdefault(item_code, 0)
|
||||
self.item_count_map[item_code] += item.stock_qty
|
||||
self.item_count_map[item_code] += flt(item.stock_qty, item.precision("stock_qty"))
|
||||
|
||||
return item_map.values()
|
||||
|
||||
|
||||
@@ -2192,16 +2192,16 @@ class StockEntry(StockController):
|
||||
d.qty -= process_loss_dict[d.item_code][1]
|
||||
|
||||
def set_serial_no_batch_for_finished_good(self):
|
||||
serial_nos = ""
|
||||
serial_nos = []
|
||||
if self.pro_doc.serial_no:
|
||||
serial_nos = self.get_serial_nos_for_fg()
|
||||
serial_nos = self.get_serial_nos_for_fg() or []
|
||||
|
||||
for row in self.items:
|
||||
if row.is_finished_item and row.item_code == self.pro_doc.production_item:
|
||||
if serial_nos:
|
||||
row.serial_no = "\n".join(serial_nos[0 : cint(row.qty)])
|
||||
|
||||
def get_serial_nos_for_fg(self, args):
|
||||
def get_serial_nos_for_fg(self):
|
||||
fields = [
|
||||
"`tabStock Entry`.`name`",
|
||||
"`tabStock Entry Detail`.`qty`",
|
||||
@@ -2217,9 +2217,7 @@ class StockEntry(StockController):
|
||||
]
|
||||
|
||||
stock_entries = frappe.get_all("Stock Entry", fields=fields, filters=filters)
|
||||
|
||||
if self.pro_doc.serial_no:
|
||||
return self.get_available_serial_nos(stock_entries)
|
||||
return self.get_available_serial_nos(stock_entries)
|
||||
|
||||
def get_available_serial_nos(self, stock_entries):
|
||||
used_serial_nos = []
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe.permissions import add_user_permission, remove_user_permission
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, nowdate, nowtime, today
|
||||
from frappe.utils import add_days, flt, now, nowdate, nowtime, today
|
||||
from six import iteritems
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||
|
||||
@@ -102,9 +102,11 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
|
||||
elif out.get("warehouse"):
|
||||
if doc and doc.get("doctype") == "Purchase Order":
|
||||
# calculate company_total_stock only for po
|
||||
bin_details = get_bin_details(args.item_code, out.warehouse, args.company)
|
||||
bin_details = get_bin_details(
|
||||
args.item_code, out.warehouse, args.company, include_child_warehouses=True
|
||||
)
|
||||
else:
|
||||
bin_details = get_bin_details(args.item_code, out.warehouse)
|
||||
bin_details = get_bin_details(args.item_code, out.warehouse, include_child_warehouses=True)
|
||||
|
||||
out.update(bin_details)
|
||||
|
||||
@@ -813,9 +815,9 @@ def insert_item_price(args):
|
||||
):
|
||||
if frappe.has_permission("Item Price", "write"):
|
||||
price_list_rate = (
|
||||
(args.rate + args.discount_amount) / args.get("conversion_factor")
|
||||
(flt(args.rate) + flt(args.discount_amount)) / args.get("conversion_factor")
|
||||
if args.get("conversion_factor")
|
||||
else (args.rate + args.discount_amount)
|
||||
else (flt(args.rate) + flt(args.discount_amount))
|
||||
)
|
||||
|
||||
item_price = frappe.db.get_value(
|
||||
@@ -1045,7 +1047,9 @@ def get_pos_profile_item_details(company, args, pos_profile=None, update_data=Fa
|
||||
res[fieldname] = pos_profile.get(fieldname)
|
||||
|
||||
if res.get("warehouse"):
|
||||
res.actual_qty = get_bin_details(args.item_code, res.warehouse).get("actual_qty")
|
||||
res.actual_qty = get_bin_details(
|
||||
args.item_code, res.warehouse, include_child_warehouses=True
|
||||
).get("actual_qty")
|
||||
|
||||
return res
|
||||
|
||||
@@ -1156,16 +1160,31 @@ def get_projected_qty(item_code, warehouse):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_bin_details(item_code, warehouse, company=None):
|
||||
bin_details = frappe.db.get_value(
|
||||
"Bin",
|
||||
{"item_code": item_code, "warehouse": warehouse},
|
||||
["projected_qty", "actual_qty", "reserved_qty"],
|
||||
as_dict=True,
|
||||
cache=True,
|
||||
) or {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0}
|
||||
def get_bin_details(item_code, warehouse, company=None, include_child_warehouses=False):
|
||||
bin_details = {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0, "ordered_qty": 0}
|
||||
|
||||
if warehouse:
|
||||
from frappe.query_builder.functions import Coalesce, Sum
|
||||
|
||||
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
|
||||
|
||||
warehouses = get_child_warehouses(warehouse) if include_child_warehouses else [warehouse]
|
||||
|
||||
bin = frappe.qb.DocType("Bin")
|
||||
bin_details = (
|
||||
frappe.qb.from_(bin)
|
||||
.select(
|
||||
Coalesce(Sum(bin.projected_qty), 0).as_("projected_qty"),
|
||||
Coalesce(Sum(bin.actual_qty), 0).as_("actual_qty"),
|
||||
Coalesce(Sum(bin.reserved_qty), 0).as_("reserved_qty"),
|
||||
Coalesce(Sum(bin.ordered_qty), 0).as_("ordered_qty"),
|
||||
)
|
||||
.where((bin.item_code == item_code) & (bin.warehouse.isin(warehouses)))
|
||||
).run(as_dict=True)[0]
|
||||
|
||||
if company:
|
||||
bin_details["company_total_stock"] = get_company_total_stock(item_code, company)
|
||||
|
||||
return bin_details
|
||||
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ def get_item_info(filters):
|
||||
if filters.get("brand"):
|
||||
conditions.append("item.brand=%(brand)s")
|
||||
conditions.append("is_stock_item = 1")
|
||||
conditions.append("disabled = 0")
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select name, item_name, description, brand, item_group,
|
||||
|
||||
@@ -1844,6 +1844,8 @@ Outstanding Amt,Offener Betrag,
|
||||
Outstanding Cheques and Deposits to clear,Ausstehende Schecks und Anzahlungen zum verbuchen,
|
||||
Outstanding for {0} cannot be less than zero ({1}),Ausstände für {0} können nicht kleiner als Null sein ({1}),
|
||||
Outward taxable supplies(zero rated),Steuerpflichtige Lieferungen aus dem Ausland (null bewertet),
|
||||
Over Receipt/Delivery of {0} {1} ignored for item {2} because you have {3} role.,"Überhöhte Annahme bzw. Lieferung von Artikel {2} mit {0} {1} wurde ignoriert, weil Sie die Rolle {3} haben."
|
||||
Overbilling of {0} {1} ignored for item {2} because you have {3} role.,"Überhöhte Abrechnung von Artikel {2} mit {0} {1} wurde ignoriert, weil Sie die Rolle {3} haben."
|
||||
Overdue,Überfällig,
|
||||
Overlap in scoring between {0} and {1},Überlappung beim Scoring zwischen {0} und {1},
|
||||
Overlapping conditions found between:,Überlagernde Bedingungen gefunden zwischen:,
|
||||
|
||||
|
Can't render this file because it is too large.
|
Reference in New Issue
Block a user