Merge pull request #32493 from frappe/version-14-hotfix

chore: release v14
This commit is contained in:
Deepesh Garg
2022-10-04 17:41:32 +05:30
committed by GitHub
42 changed files with 827 additions and 474 deletions

View File

@@ -99,7 +99,7 @@ class BankClearance(Document):
.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)
.orderby(loan_disbursement.name, order=frappe.qb.desc)
).run(as_dict=1)
loan_repayment = frappe.qb.DocType("Loan Repayment")
@@ -126,7 +126,9 @@ class BankClearance(Document):
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
query = query.where((loan_repayment.repay_from_salary == 0))
query = query.orderby(loan_repayment.posting_date).orderby(loan_repayment.name, frappe.qb.desc)
query = query.orderby(loan_repayment.posting_date).orderby(
loan_repayment.name, order=frappe.qb.desc
)
loan_repayments = query.run(as_dict=True)

View File

@@ -186,8 +186,10 @@
{
"fetch_from": "bank_account.bank",
"fieldname": "bank",
"fieldtype": "Read Only",
"label": "Bank"
"fieldtype": "Link",
"label": "Bank",
"options": "Bank",
"read_only": 1
},
{
"fetch_from": "bank_account.bank_account_no",
@@ -366,10 +368,11 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-09-18 12:24:14.178853",
"modified": "2022-09-30 16:19:43.680025",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Request",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
@@ -401,5 +404,6 @@
}
],
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"states": []
}

View File

@@ -98,7 +98,6 @@
"section_break_44",
"apply_discount_on",
"base_discount_amount",
"additional_discount_account",
"column_break_46",
"additional_discount_percentage",
"discount_amount",
@@ -1387,12 +1386,6 @@
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "additional_discount_account",
"fieldtype": "Link",
"label": "Additional Discount Account",
"options": "Account"
},
{
"default": "0",
"fieldname": "ignore_default_payment_terms_template",
@@ -1445,7 +1438,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2022-09-13 23:39:54.525037",
"modified": "2022-09-27 11:07:55.766844",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -669,9 +669,6 @@ class PurchaseInvoice(BuyingController):
exchange_rate_map, net_rate_map = get_purchase_document_details(self)
enable_discount_accounting = cint(
frappe.db.get_single_value("Buying Settings", "enable_discount_accounting")
)
provisional_accounting_for_non_stock_items = cint(
frappe.db.get_value(
"Company", self.company, "enable_provisional_accounting_for_non_stock_items"
@@ -1159,9 +1156,6 @@ class PurchaseInvoice(BuyingController):
def make_tax_gl_entries(self, gl_entries):
# tax table gl entries
valuation_tax = {}
enable_discount_accounting = cint(
frappe.db.get_single_value("Buying Settings", "enable_discount_accounting")
)
for tax in self.get("taxes"):
amount, base_amount = self.get_tax_amounts(tax, None)
@@ -1249,15 +1243,6 @@ class PurchaseInvoice(BuyingController):
)
)
@property
def enable_discount_accounting(self):
if not hasattr(self, "_enable_discount_accounting"):
self._enable_discount_accounting = cint(
frappe.db.get_single_value("Buying Settings", "enable_discount_accounting")
)
return self._enable_discount_accounting
def make_internal_transfer_gl_entries(self, gl_entries):
if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges):
account_currency = get_account_currency(self.unrealized_profit_loss_account)

View File

@@ -74,7 +74,6 @@
"manufacturer_part_no",
"accounting",
"expense_account",
"discount_account",
"col_break5",
"is_fixed_asset",
"asset_location",
@@ -860,12 +859,6 @@
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "discount_account",
"fieldtype": "Link",
"label": "Discount Account",
"options": "Account"
},
{
"fieldname": "product_bundle",
"fieldtype": "Link",
@@ -877,7 +870,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2022-06-17 05:31:10.520171",
"modified": "2022-09-27 10:54:23.980713",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@@ -22,9 +22,12 @@ from erpnext.accounts.general_ledger import get_round_off_account_and_cost_cente
from erpnext.accounts.party import get_due_date, get_party_account, get_party_details
from erpnext.accounts.utils import get_account_currency
from erpnext.assets.doctype.asset.depreciation import (
depreciate_asset,
get_disposal_account_and_cost_center,
get_gl_entries_on_asset_disposal,
get_gl_entries_on_asset_regain,
reset_depreciation_schedule,
reverse_depreciation_entry_made_after_disposal,
)
from erpnext.controllers.accounts_controller import validate_account_head
from erpnext.controllers.selling_controller import SellingController
@@ -1086,18 +1089,20 @@ class SalesInvoice(SellingController):
asset.db_set("disposal_date", None)
if asset.calculate_depreciation:
self.reverse_depreciation_entry_made_after_disposal(asset)
self.reset_depreciation_schedule(asset)
posting_date = frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
reverse_depreciation_entry_made_after_disposal(asset, posting_date)
reset_depreciation_schedule(asset, self.posting_date)
else:
if asset.calculate_depreciation:
depreciate_asset(asset, self.posting_date)
asset.reload()
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name")
)
asset.db_set("disposal_date", self.posting_date)
if asset.calculate_depreciation:
self.depreciate_asset(asset)
for gle in fixed_asset_gl_entries:
gle["against"] = self.customer
gl_entries.append(self.get_gl_dict(gle, item=item))

View File

@@ -8,7 +8,7 @@ import frappe
from frappe.model.dynamic_links import get_dynamic_link_map
from frappe.model.naming import make_autoname
from frappe.tests.utils import change_settings
from frappe.utils import add_days, flt, getdate, nowdate
from frappe.utils import add_days, flt, getdate, nowdate, today
import erpnext
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
@@ -3196,6 +3196,37 @@ class TestSalesInvoice(unittest.TestCase):
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled
)
def test_batch_expiry_for_sales_invoice_return(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
from erpnext.stock.doctype.item.test_item import make_item
item = make_item(
"_Test Batch Item For Return Check",
{
"is_purchase_item": 1,
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "TBIRC.#####",
},
)
pr = make_purchase_receipt(qty=1, item_code=item.name)
batch_no = pr.items[0].batch_no
si = create_sales_invoice(qty=1, item_code=item.name, update_stock=1, batch_no=batch_no)
si.load_from_db()
batch_no = si.items[0].batch_no
self.assertTrue(batch_no)
frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1))
return_si = make_return_doc(si.doctype, si.name)
return_si.save().submit()
self.assertTrue(return_si.docstatus == 1)
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()
@@ -3289,6 +3320,7 @@ def create_sales_invoice(**args):
"serial_no": args.serial_no,
"conversion_factor": 1,
"incoming_rate": args.incoming_rate or 0,
"batch_no": args.batch_no or None,
},
)

View File

@@ -335,6 +335,9 @@ def get_advance_vouchers(
"party": ["in", parties],
}
if party_type == "Customer":
filters.update({"against_voucher": ["is", "not set"]})
if company:
filters["company"] = company
if from_date and to_date:

View File

@@ -370,7 +370,7 @@ def get_conditions(filters):
where parent=`tabSales Invoice`.name
and ifnull(`tab{table}`.{field}, '') = %({field})s)"""
conditions += get_sales_invoice_item_field_condition("mode_of_payments", "Sales Invoice Payment")
conditions += get_sales_invoice_item_field_condition("mode_of_payment", "Sales Invoice Payment")
conditions += get_sales_invoice_item_field_condition("cost_center")
conditions += get_sales_invoice_item_field_condition("warehouse")
conditions += get_sales_invoice_item_field_condition("brand")

View File

@@ -86,7 +86,7 @@ def get_fiscal_years(
)
)
query = query.orderby(FY.year_start_date, Order.desc)
query = query.orderby(FY.year_start_date, order=Order.desc)
fiscal_years = query.run(as_dict=True)
frappe.cache().hset("fiscal_years", company, fiscal_years)

View File

@@ -388,7 +388,7 @@
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt",
"options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nDecapitalized",
"read_only": 1
},
{

View File

@@ -828,7 +828,9 @@ class Asset(AccountsController):
def update_maintenance_status():
assets = frappe.get_all("Asset", filters={"docstatus": 1, "maintenance_required": 1})
assets = frappe.get_all(
"Asset", filters={"docstatus": 1, "maintenance_required": 1, "disposal_date": ("is", "not set")}
)
for asset in assets:
asset = frappe.get_doc("Asset", asset.name)

View File

@@ -4,11 +4,12 @@
import frappe
from frappe import _
from frappe.utils import cint, flt, getdate, today
from frappe.utils import add_months, cint, flt, getdate, nowdate, today
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
)
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
def post_depreciation_entries(date=None, commit=True):
@@ -196,6 +197,11 @@ def scrap_asset(asset_name):
_("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status)
)
date = today()
depreciate_asset(asset, date)
asset.reload()
depreciation_series = frappe.get_cached_value(
"Company", asset.company, "series_for_depreciation_entry"
)
@@ -203,7 +209,7 @@ def scrap_asset(asset_name):
je = frappe.new_doc("Journal Entry")
je.voucher_type = "Journal Entry"
je.naming_series = depreciation_series
je.posting_date = today()
je.posting_date = date
je.company = asset.company
je.remark = "Scrap Entry for asset {0}".format(asset_name)
@@ -214,7 +220,7 @@ def scrap_asset(asset_name):
je.flags.ignore_permissions = True
je.submit()
frappe.db.set_value("Asset", asset_name, "disposal_date", today())
frappe.db.set_value("Asset", asset_name, "disposal_date", date)
frappe.db.set_value("Asset", asset_name, "journal_entry_for_scrap", je.name)
asset.set_status("Scrapped")
@@ -225,6 +231,9 @@ def scrap_asset(asset_name):
def restore_asset(asset_name):
asset = frappe.get_doc("Asset", asset_name)
reverse_depreciation_entry_made_after_disposal(asset, asset.disposal_date)
reset_depreciation_schedule(asset, asset.disposal_date)
je = asset.journal_entry_for_scrap
asset.db_set("disposal_date", None)
@@ -235,6 +244,91 @@ def restore_asset(asset_name):
asset.set_status()
def depreciate_asset(asset, date):
asset.flags.ignore_validate_update_after_submit = True
asset.prepare_depreciation_data(date_of_disposal=date)
asset.save()
make_depreciation_entry(asset.name, date)
def reset_depreciation_schedule(asset, date):
asset.flags.ignore_validate_update_after_submit = True
# recreate original depreciation schedule of the asset
asset.prepare_depreciation_data(date_of_return=date)
modify_depreciation_schedule_for_asset_repairs(asset)
asset.save()
def modify_depreciation_schedule_for_asset_repairs(asset):
asset_repairs = frappe.get_all(
"Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"]
)
for repair in asset_repairs:
if repair.increase_in_asset_life:
asset_repair = frappe.get_doc("Asset Repair", repair.name)
asset_repair.modify_depreciation_schedule()
asset.prepare_depreciation_data()
def reverse_depreciation_entry_made_after_disposal(asset, date):
row = -1
finance_book = asset.get("schedules")[0].get("finance_book")
for schedule in asset.get("schedules"):
if schedule.finance_book != finance_book:
row = 0
finance_book = schedule.finance_book
else:
row += 1
if schedule.schedule_date == date:
if not disposal_was_made_on_original_schedule_date(
asset, schedule, row, date
) or disposal_happens_in_the_future(date):
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
reverse_journal_entry.posting_date = nowdate()
frappe.flags.is_reverse_depr_entry = True
reverse_journal_entry.submit()
frappe.flags.is_reverse_depr_entry = False
asset.flags.ignore_validate_update_after_submit = True
schedule.journal_entry = None
depreciation_amount = get_depreciation_amount_in_je(reverse_journal_entry)
asset.finance_books[0].value_after_depreciation += depreciation_amount
asset.save()
def get_depreciation_amount_in_je(journal_entry):
if journal_entry.accounts[0].debit_in_account_currency:
return journal_entry.accounts[0].debit_in_account_currency
else:
return journal_entry.accounts[0].credit_in_account_currency
# if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone
def disposal_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_disposal):
for finance_book in asset.get("finance_books"):
if schedule.finance_book == finance_book.finance_book:
orginal_schedule_date = add_months(
finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation)
)
if orginal_schedule_date == posting_date_of_disposal:
return True
return False
def disposal_happens_in_the_future(posting_date_of_disposal):
if posting_date_of_disposal > getdate():
return True
return False
def get_gl_entries_on_asset_regain(
asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None
):

View File

@@ -4,10 +4,23 @@
import unittest
import frappe
from frappe.utils import add_days, add_months, cstr, flt, get_last_day, getdate, nowdate
from frappe.utils import (
add_days,
add_months,
cstr,
flt,
get_first_day,
get_last_day,
getdate,
nowdate,
)
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.assets.doctype.asset.asset import make_sales_invoice, split_asset
from erpnext.assets.doctype.asset.asset import (
make_sales_invoice,
split_asset,
update_maintenance_status,
)
from erpnext.assets.doctype.asset.depreciation import (
post_depreciation_entries,
restore_asset,
@@ -178,28 +191,48 @@ class TestAsset(AssetSetup):
self.assertEqual(doc.items[0].is_fixed_asset, 1)
def test_scrap_asset(self):
date = nowdate()
purchase_date = add_months(get_first_day(date), -2)
asset = create_asset(
calculate_depreciation=1,
available_for_use_date="2020-01-01",
purchase_date="2020-01-01",
available_for_use_date=purchase_date,
purchase_date=purchase_date,
expected_value_after_useful_life=10000,
total_number_of_depreciations=10,
frequency_of_depreciation=1,
submit=1,
)
post_depreciation_entries(date=add_months("2020-01-01", 4))
post_depreciation_entries(date=add_months(purchase_date, 2))
asset.load_from_db()
accumulated_depr_amount = flt(
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
asset.precision("gross_purchase_amount"),
)
self.assertEquals(accumulated_depr_amount, 18000.0)
scrap_asset(asset.name)
asset.load_from_db()
accumulated_depr_amount = flt(
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
asset.precision("gross_purchase_amount"),
)
pro_rata_amount, _, _ = asset.get_pro_rata_amt(
asset.finance_books[0], 9000, add_months(get_last_day(purchase_date), 1), date
)
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
self.assertEquals(accumulated_depr_amount, 18000.00 + pro_rata_amount)
self.assertEqual(asset.status, "Scrapped")
self.assertTrue(asset.journal_entry_for_scrap)
expected_gle = (
("_Test Accumulated Depreciations - _TC", 36000.0, 0.0),
("_Test Accumulated Depreciations - _TC", 18000.0 + pro_rata_amount, 0.0),
("_Test Fixed Asset - _TC", 0.0, 100000.0),
("_Test Gain/Loss on Asset Disposal - _TC", 64000.0, 0.0),
("_Test Gain/Loss on Asset Disposal - _TC", 82000.0 - pro_rata_amount, 0.0),
)
gle = frappe.db.sql(
@@ -216,7 +249,64 @@ class TestAsset(AssetSetup):
self.assertFalse(asset.journal_entry_for_scrap)
self.assertEqual(asset.status, "Partially Depreciated")
accumulated_depr_amount = flt(
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
asset.precision("gross_purchase_amount"),
)
this_month_depr_amount = 9000.0 if get_last_day(date) == date else 0
self.assertEquals(accumulated_depr_amount, 18000.0 + this_month_depr_amount)
def test_gle_made_by_asset_sale(self):
date = nowdate()
purchase_date = add_months(get_first_day(date), -2)
asset = create_asset(
calculate_depreciation=1,
available_for_use_date=purchase_date,
purchase_date=purchase_date,
expected_value_after_useful_life=10000,
total_number_of_depreciations=10,
frequency_of_depreciation=1,
submit=1,
)
post_depreciation_entries(date=add_months(purchase_date, 2))
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
si.customer = "_Test Customer"
si.due_date = nowdate()
si.get("items")[0].rate = 25000
si.insert()
si.submit()
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
pro_rata_amount, _, _ = asset.get_pro_rata_amt(
asset.finance_books[0], 9000, add_months(get_last_day(purchase_date), 1), date
)
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
expected_gle = (
("_Test Accumulated Depreciations - _TC", 18000.0 + pro_rata_amount, 0.0),
("_Test Fixed Asset - _TC", 0.0, 100000.0),
("_Test Gain/Loss on Asset Disposal - _TC", 57000.0 - pro_rata_amount, 0.0),
("Debtors - _TC", 25000.0, 0.0),
)
gle = frappe.db.sql(
"""select account, debit, credit from `tabGL Entry`
where voucher_type='Sales Invoice' and voucher_no = %s
order by account""",
si.name,
)
self.assertSequenceEqual(gle, expected_gle)
si.cancel()
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
def test_asset_with_maintenance_required_status_after_sale(self):
asset = create_asset(
calculate_depreciation=1,
available_for_use_date="2020-06-06",
@@ -224,6 +314,7 @@ class TestAsset(AssetSetup):
expected_value_after_useful_life=10000,
total_number_of_depreciations=3,
frequency_of_depreciation=10,
maintenance_required=1,
depreciation_start_date="2020-12-31",
submit=1,
)
@@ -239,24 +330,9 @@ class TestAsset(AssetSetup):
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
expected_gle = (
("_Test Accumulated Depreciations - _TC", 20490.2, 0.0),
("_Test Fixed Asset - _TC", 0.0, 100000.0),
("_Test Gain/Loss on Asset Disposal - _TC", 54509.8, 0.0),
("Debtors - _TC", 25000.0, 0.0),
)
update_maintenance_status()
gle = frappe.db.sql(
"""select account, debit, credit from `tabGL Entry`
where voucher_type='Sales Invoice' and voucher_no = %s
order by account""",
si.name,
)
self.assertSequenceEqual(gle, expected_gle)
si.cancel()
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
def test_asset_splitting(self):
asset = create_asset(
@@ -1376,6 +1452,7 @@ def create_asset(**args):
"number_of_depreciations_booked": args.number_of_depreciations_booked or 0,
"gross_purchase_amount": args.gross_purchase_amount or 100000,
"purchase_receipt_amount": args.purchase_receipt_amount or 100000,
"maintenance_required": args.maintenance_required or 0,
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"available_for_use_date": args.available_for_use_date or "2020-06-06",
"location": args.location or "Test Location",

View File

@@ -12,8 +12,11 @@ from six import string_types
import erpnext
from erpnext.assets.doctype.asset.depreciation import (
depreciate_asset,
get_gl_entries_on_asset_disposal,
get_value_after_depreciation_on_disposal_date,
reset_depreciation_schedule,
reverse_depreciation_entry_made_after_disposal,
)
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import (
@@ -424,7 +427,7 @@ class AssetCapitalization(StockController):
asset = self.get_asset(item)
if asset.calculate_depreciation:
self.depreciate_asset(asset)
depreciate_asset(asset, self.posting_date)
asset.reload()
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
@@ -520,8 +523,8 @@ class AssetCapitalization(StockController):
self.set_consumed_asset_status(asset)
if asset.calculate_depreciation:
self.reverse_depreciation_entry_made_after_disposal(asset)
self.reset_depreciation_schedule(asset)
reverse_depreciation_entry_made_after_disposal(asset, self.posting_date)
reset_depreciation_schedule(asset, self.posting_date)
def get_asset(self, item):
asset = frappe.get_doc("Asset", item.asset)

View File

@@ -20,7 +20,6 @@
"maintain_same_rate",
"allow_multiple_items",
"bill_for_rejected_quantity_in_purchase_invoice",
"enable_discount_accounting",
"subcontract",
"backflush_raw_materials_of_subcontract_based_on",
"column_break_11",
@@ -134,13 +133,6 @@
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account",
"fieldname": "enable_discount_accounting",
"fieldtype": "Check",
"label": "Enable Discount Accounting for Buying"
}
],
"icon": "fa fa-cog",
@@ -148,7 +140,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2022-09-01 18:01:34.994657",
"modified": "2022-09-27 10:50:27.050252",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",

View File

@@ -5,15 +5,10 @@
import frappe
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.model.document import Document
from frappe.utils import cint
class BuyingSettings(Document):
def on_update(self):
self.toggle_discount_accounting_fields()
def validate(self):
for key in ["supplier_group", "supp_master_name", "maintain_same_rate", "buying_price_list"]:
frappe.db.set_default(key, self.get(key, ""))
@@ -26,60 +21,3 @@ class BuyingSettings(Document):
self.get("supp_master_name") == "Naming Series",
hide_name_field=False,
)
def toggle_discount_accounting_fields(self):
enable_discount_accounting = cint(self.enable_discount_accounting)
make_property_setter(
"Purchase Invoice Item",
"discount_account",
"hidden",
not (enable_discount_accounting),
"Check",
validate_fields_for_doctype=False,
)
if enable_discount_accounting:
make_property_setter(
"Purchase Invoice Item",
"discount_account",
"mandatory_depends_on",
"eval: doc.discount_amount",
"Code",
validate_fields_for_doctype=False,
)
else:
make_property_setter(
"Purchase Invoice Item",
"discount_account",
"mandatory_depends_on",
"",
"Code",
validate_fields_for_doctype=False,
)
make_property_setter(
"Purchase Invoice",
"additional_discount_account",
"hidden",
not (enable_discount_accounting),
"Check",
validate_fields_for_doctype=False,
)
if enable_discount_accounting:
make_property_setter(
"Purchase Invoice",
"additional_discount_account",
"mandatory_depends_on",
"eval: doc.discount_amount",
"Code",
validate_fields_for_doctype=False,
)
else:
make_property_setter(
"Purchase Invoice",
"additional_discount_account",
"mandatory_depends_on",
"",
"Code",
validate_fields_for_doctype=False,
)

View File

@@ -33,6 +33,7 @@ frappe.ui.form.on("Purchase Order", {
frm.set_query("fg_item", "items", function() {
return {
filters: {
'is_stock_item': 1,
'is_sub_contracted_item': 1,
'default_bom': ['!=', '']
}

View File

@@ -38,7 +38,6 @@ from erpnext.accounts.party import (
validate_party_frozen_disabled,
)
from erpnext.accounts.utils import get_account_currency, get_fiscal_years, validate_fiscal_year
from erpnext.assets.doctype.asset.depreciation import make_depreciation_entry
from erpnext.buying.utils import update_last_purchase_rate
from erpnext.controllers.print_settings import (
set_print_templates_for_item_table,
@@ -1891,88 +1890,6 @@ class AccountsController(TransactionBase):
_("Select finance book for the item {0} at row {1}").format(item.item_code, item.idx)
)
def depreciate_asset(self, asset):
asset.flags.ignore_validate_update_after_submit = True
asset.prepare_depreciation_data(date_of_disposal=self.posting_date)
asset.save()
make_depreciation_entry(asset.name, self.posting_date)
def reset_depreciation_schedule(self, asset):
asset.flags.ignore_validate_update_after_submit = True
# recreate original depreciation schedule of the asset
asset.prepare_depreciation_data(date_of_return=self.posting_date)
self.modify_depreciation_schedule_for_asset_repairs(asset)
asset.save()
def modify_depreciation_schedule_for_asset_repairs(self, asset):
asset_repairs = frappe.get_all(
"Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"]
)
for repair in asset_repairs:
if repair.increase_in_asset_life:
asset_repair = frappe.get_doc("Asset Repair", repair.name)
asset_repair.modify_depreciation_schedule()
asset.prepare_depreciation_data()
def reverse_depreciation_entry_made_after_disposal(self, asset):
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
posting_date_of_original_disposal = self.get_posting_date_of_disposal_entry()
row = -1
finance_book = asset.get("schedules")[0].get("finance_book")
for schedule in asset.get("schedules"):
if schedule.finance_book != finance_book:
row = 0
finance_book = schedule.finance_book
else:
row += 1
if schedule.schedule_date == posting_date_of_original_disposal:
if not self.disposal_was_made_on_original_schedule_date(
asset, schedule, row, posting_date_of_original_disposal
) or self.disposal_happens_in_the_future(posting_date_of_original_disposal):
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
reverse_journal_entry.posting_date = nowdate()
frappe.flags.is_reverse_depr_entry = True
reverse_journal_entry.submit()
frappe.flags.is_reverse_depr_entry = False
asset.flags.ignore_validate_update_after_submit = True
schedule.journal_entry = None
asset.save()
def get_posting_date_of_disposal_entry(self):
if self.doctype == "Sales Invoice" and self.return_against:
return frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
else:
return self.posting_date
# if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone
def disposal_was_made_on_original_schedule_date(
self, asset, schedule, row, posting_date_of_disposal
):
for finance_book in asset.get("finance_books"):
if schedule.finance_book == finance_book.finance_book:
orginal_schedule_date = add_months(
finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation)
)
if orginal_schedule_date == posting_date_of_disposal:
return True
return False
def disposal_happens_in_the_future(self, posting_date_of_disposal):
if posting_date_of_disposal > getdate():
return True
return False
@frappe.whitelist()
def get_tax_rate(account_head):

View File

@@ -212,21 +212,15 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
meta = frappe.get_meta(doctype, cached=True)
searchfields = meta.get_search_fields()
# these are handled separately
ignored_search_fields = ("item_name", "description")
for ignored_field in ignored_search_fields:
if ignored_field in searchfields:
searchfields.remove(ignored_field)
columns = ""
extra_searchfields = [
field
for field in searchfields
if not field in ["name", "item_group", "description", "item_name"]
]
extra_searchfields = [field for field in searchfields if not field in ["name", "description"]]
if extra_searchfields:
columns = ", " + ", ".join(extra_searchfields)
columns += ", " + ", ".join(extra_searchfields)
if "description" in searchfields:
columns += """, if(length(tabItem.description) > 40, \
concat(substr(tabItem.description, 1, 40), "..."), description) as description"""
searchfields = searchfields + [
field
@@ -266,12 +260,10 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
if frappe.db.count(doctype, cache=True) < 50000:
# scan description only if items are less than 50000
description_cond = "or tabItem.description LIKE %(txt)s"
return frappe.db.sql(
"""select
tabItem.name, tabItem.item_name, tabItem.item_group,
if(length(tabItem.description) > 40, \
concat(substr(tabItem.description, 1, 40), "..."), description) as description
{columns}
tabItem.name {columns}
from tabItem
where tabItem.docstatus < 2
and tabItem.disabled=0

View File

@@ -69,9 +69,18 @@ class SubcontractingController(StockController):
def validate_items(self):
for item in self.items:
if not frappe.get_value("Item", item.item_code, "is_sub_contracted_item"):
is_stock_item, is_sub_contracted_item = frappe.get_value(
"Item", item.item_code, ["is_stock_item", "is_sub_contracted_item"]
)
if not is_stock_item:
msg = f"Item {item.item_name} must be a stock item."
frappe.throw(_(msg))
if not is_sub_contracted_item:
msg = f"Item {item.item_name} must be a subcontracted item."
frappe.throw(_(msg))
if item.bom:
bom = frappe.get_doc("BOM", item.bom)
if not bom.is_active:
@@ -841,7 +850,7 @@ def make_rm_stock_entry(
for fg_item_code in fg_item_code_list:
for rm_item in rm_items:
if rm_item.get("main_item_code") or rm_item.get("item_code") == fg_item_code:
if rm_item.get("main_item_code") == fg_item_code or rm_item.get("item_code") == fg_item_code:
rm_item_code = rm_item.get("rm_item_code")
items_dict = {

View File

@@ -508,6 +508,7 @@ accounting_dimension_doctypes = [
"Landed Cost Item",
"Asset Value Adjustment",
"Asset Repair",
"Asset Capitalization",
"Loyalty Program",
"Stock Reconciliation",
"POS Profile",

View File

@@ -315,3 +315,4 @@ erpnext.patches.v14_0.fix_crm_no_of_employees
erpnext.patches.v14_0.create_accounting_dimensions_in_subcontracting_doctypes
erpnext.patches.v14_0.fix_subcontracting_receipt_gl_entries
erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger
erpnext.patches.v14_0.create_accounting_dimensions_for_asset_capitalization

View File

@@ -100,6 +100,7 @@ def execute():
"mode_of_payment": loan.mode_of_payment,
"loan_account": loan.loan_account,
"payment_account": loan.payment_account,
"disbursement_account": loan.payment_account,
"interest_income_account": loan.interest_income_account,
"penalty_income_account": loan.penalty_income_account,
},
@@ -190,6 +191,7 @@ def create_loan_type(loan, loan_type_name, penalty_account):
loan_type_doc.company = loan.company
loan_type_doc.mode_of_payment = loan.mode_of_payment
loan_type_doc.payment_account = loan.payment_account
loan_type_doc.disbursement_account = loan.payment_account
loan_type_doc.loan_account = loan.loan_account
loan_type_doc.interest_income_account = loan.interest_income_account
loan_type_doc.penalty_income_account = penalty_account

View File

@@ -0,0 +1,31 @@
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
doctype = "Asset Capitalization"
for d in accounting_dimensions:
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": "accounting_dimensions_section",
}
create_custom_field(doctype, df, ignore_validate=True)
frappe.clear_cache(doctype=doctype)

View File

@@ -0,0 +1,115 @@
#page-order {
.main-column {
.page-content-wrapper {
.breadcrumb-container {
@media screen and (min-width: 567px) {
padding-left: var(--padding-sm);
}
}
.container.my-4 {
background-color: var(--fg-color);
@media screen and (min-width: 567px) {
padding: 1.25rem 1.5rem;
border-radius: var(--border-radius-md);
box-shadow: var(--card-shadow);
}
}
}
}
}
.indicator-container {
@media screen and (max-width: 567px) {
padding-bottom: 0.8rem;
}
}
.order-items {
padding: 1.5rem 0;
border-bottom: 1px solid var(--border-color);
color: var(--gray-700);
@media screen and (max-width: 567px) {
align-items: flex-start !important;
}
.col-2 {
@media screen and (max-width: 567px) {
flex: auto;
max-width: 28%;
}
}
.order-item-name {
font-size: var(--text-base);
font-weight: 500;
}
.btn:focus,
.btn:hover {
background-color: var(--control-bg);
}
.col-6 {
@media screen and (max-width: 567px) {
max-width: 100%;
}
&.order-item-name {
font-size: var(--text-base);
}
}
}
.item-grand-total {
font-size: var(--text-base);
}
.list-item-name,
.item-total,
.order-container,
.order-qty {
font-size: var(--text-md);
}
.d-s-n {
@media screen and (max-width: 567px) {
display: none;
}
}
.d-l-n {
@media screen and (min-width: 567px) {
display: none;
}
}
.border-btm {
border-bottom: 1px solid var(--border-color);
}
.order-taxes {
display: flex;
@media screen and (min-width: 567px) {
justify-content: flex-end;
}
.col-4 {
padding-right: 0;
.col-8 {
padding-left: 0;
padding-right: 0;
}
@media screen and (max-width: 567px) {
padding-left: 0;
flex: auto;
max-width: 100%;
}
}
}

View File

@@ -1,3 +1,4 @@
@import './order-page';
.filter-options {
max-height: 300px;
@@ -32,19 +33,29 @@
height: 24px;
}
.website-list .result {
margin-top: 2rem;
}
.website-list {
background-color: var(--fg-color);
padding: 0 var(--padding-lg);
border-radius: var(--border-radius-md);
.result {
border-bottom: 1px solid var(--border-color);
@media screen and (max-width: 567px) {
margin-left: -2rem;
}
&.result {
border-bottom: 1px solid var(--border-color);
}
}
.transaction-list-item {
padding: 1rem 0;
border-top: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color);
position: relative;
&:only-child, &:last-child {
border: 0;
}
a.transaction-item-link {
position: absolute;
top: 0;
@@ -68,3 +79,13 @@
line-height: 1.3;
}
}
.list-item-name, .item-total {
font-size: var(--font-size-sm);
}
.items-preview {
@media screen and (max-width: 567px) {
margin-top: 1rem;
}
}

View File

@@ -6,7 +6,7 @@ import json
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import cstr, flt, nowdate, nowtime
from frappe.utils import add_days, cstr, flt, nowdate, nowtime, today
from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.accounts.utils import get_balance_on
@@ -1091,6 +1091,36 @@ class TestDeliveryNote(FrappeTestCase):
frappe.db.exists("GL Entry", {"voucher_no": dn.name, "voucher_type": dn.doctype})
)
def test_batch_expiry_for_delivery_note(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
item = make_item(
"_Test Batch Item For Return Check",
{
"is_purchase_item": 1,
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "TBIRC.#####",
},
)
pi = make_purchase_receipt(qty=1, item_code=item.name)
dn = create_delivery_note(qty=1, item_code=item.name, batch_no=pi.items[0].batch_no)
dn.load_from_db()
batch_no = dn.items[0].batch_no
self.assertTrue(batch_no)
frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1))
return_dn = make_return_doc(dn.doctype, dn.name)
return_dn.save().submit()
self.assertTrue(return_dn.docstatus == 1)
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")
@@ -1117,6 +1147,7 @@ def create_delivery_note(**args):
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"serial_no": args.serial_no,
"batch_no": args.batch_no or None,
"target_warehouse": args.target_warehouse,
},
)

View File

@@ -10,6 +10,31 @@ frappe.ui.form.on("Item", {
frm.add_fetch('attribute', 'to_range', 'to_range');
frm.add_fetch('attribute', 'increment', 'increment');
frm.add_fetch('tax_type', 'tax_rate', 'tax_rate');
frm.make_methods = {
'Sales Order': () => {
open_form(frm, "Sales Order", "Sales Order Item", "items");
},
'Delivery Note': () => {
open_form(frm, "Delivery Note", "Delivery Note Item", "items");
},
'Sales Invoice': () => {
open_form(frm, "Sales Invoice", "Sales Invoice Item", "items");
},
'Purchase Order': () => {
open_form(frm, "Purchase Order", "Purchase Order Item", "items");
},
'Purchase Receipt': () => {
open_form(frm, "Purchase Receipt", "Purchase Receipt Item", "items");
},
'Purchase Invoice': () => {
open_form(frm, "Purchase Invoice", "Purchase Invoice Item", "items");
},
'Material Request': () => {
open_form(frm, "Material Request", "Material Request Item", "items");
},
};
},
onload: function(frm) {
erpnext.item.setup_queries(frm);
@@ -858,3 +883,17 @@ frappe.tour['Item'] = [
];
function open_form(frm, doctype, child_doctype, parentfield) {
frappe.model.with_doctype(doctype, () => {
let new_doc = frappe.model.get_new_doc(doctype);
let new_child_doc = frappe.model.add_child(new_doc, child_doctype, parentfield);
new_child_doc.item_code = frm.doc.name;
new_child_doc.item_name = frm.doc.item_name;
new_child_doc.uom = frm.doc.stock_uom;
new_child_doc.description = frm.doc.description;
frappe.ui.form.make_quick_entry(doctype, null, null, new_doc);
});
}

View File

@@ -937,17 +937,21 @@ class Item(Document):
"Purchase Order Item",
"Material Request Item",
"Product Bundle",
"BOM",
]
for doctype in linked_doctypes:
filters = {"item_code": self.name, "docstatus": 1}
if doctype == "Product Bundle":
filters = {"new_item_code": self.name}
if doctype in ("Product Bundle", "BOM"):
if doctype == "Product Bundle":
filters = {"new_item_code": self.name}
fieldname = "new_item_code as docname"
else:
filters = {"item": self.name, "docstatus": 1}
fieldname = "name as docname"
if linked_doc := frappe.db.get_value(
doctype, filters, ["new_item_code as docname"], as_dict=True
):
if linked_doc := frappe.db.get_value(doctype, filters, fieldname, as_dict=True):
return linked_doc.update({"doctype": doctype})
elif doctype in (

View File

@@ -5,6 +5,7 @@
import json
import frappe
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.test_runner import make_test_objects
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, today
@@ -816,6 +817,30 @@ class TestItem(FrappeTestCase):
item.reload()
self.assertEqual(item.is_stock_item, 1)
def test_serach_fields_for_item(self):
from erpnext.controllers.queries import item_query
make_property_setter("Item", None, "search_fields", "item_name", "Data", for_doctype="Doctype")
item = make_item(properties={"item_name": "Test Item", "description": "Test Description"})
data = item_query(
"Item", "Test Item", "", 0, 20, filters={"item_name": "Test Item"}, as_dict=True
)
self.assertEqual(data[0].name, item.name)
self.assertEqual(data[0].item_name, item.item_name)
self.assertTrue("description" not in data[0])
make_property_setter(
"Item", None, "search_fields", "item_name, description", "Data", for_doctype="Doctype"
)
data = item_query(
"Item", "Test Item", "", 0, 20, filters={"item_name": "Test Item"}, as_dict=True
)
self.assertEqual(data[0].name, item.name)
self.assertEqual(data[0].item_name, item.item_name)
self.assertEqual(data[0].description, item.description)
self.assertTrue("description" in data[0])
def set_item_variant_settings(fields):
doc = frappe.get_doc("Item Variant Settings")

View File

@@ -183,7 +183,7 @@ class PickList(Document):
frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx))
item_code = item.item_code
reference = item.sales_order_item or item.material_request_item
key = (item_code, item.uom, item.warehouse, reference)
key = (item_code, item.uom, item.warehouse, item.batch_no, reference)
item.idx = None
item.name = None

View File

@@ -153,7 +153,9 @@ class StockLedgerEntry(Document):
def validate_batch(self):
if self.batch_no and self.voucher_type != "Stock Entry":
if self.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and self.actual_qty < 0:
if (self.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and self.actual_qty < 0) or (
self.voucher_type in ["Delivery Note", "Sales Invoice"] and self.actual_qty > 0
):
return
expiry_date = frappe.db.get_value("Batch", self.batch_no, "expiry_date")

View File

@@ -6,7 +6,7 @@
aria-label="{{ _('Your email address...') }}"
aria-describedby="footer-subscribe-button">
<div class="input-group-append">
<button class="btn btn-sm btn-default"
<button class="btn btn-sm btn-secondary pl-3 pr-3 ml-2"
type="button" id="footer-subscribe-button">{{ _("Get Updates") }}</button>
</div>
</div>

View File

@@ -1,5 +1,5 @@
{% macro product_image_square(website_image, css_class="") %}
<div class="product-image product-image-square
<div class="product-image product-image-square h-100 rounded
{% if not website_image -%} missing-image {%- endif %} {{ css_class }}"
{% if website_image -%}
style="background-image: url('{{ frappe.utils.quoted(website_image) | abs_url }}');"

View File

@@ -3,7 +3,7 @@
{% macro item_name_and_description(d) %}
<div class="row item_name_and_description">
<div class="col-xs-4 col-sm-2 order-image-col">
<div class="order-image">
<div class="order-image h-100">
{% if d.thumbnail or d.image %}
{{ product_image(d.thumbnail or d.image, no_border=True) }}
{% else %}
@@ -18,6 +18,9 @@
<div class="text-muted small item-description">
{{ html2text(d.description) | truncate(140) }}
</div>
<span class="text-muted mt-2 d-l-n order-qty">
{{ _("Qty ") }}({{ d.get_formatted("qty") }})
</span>
</div>
</div>
{% endmacro %}

View File

@@ -1,84 +1,111 @@
{% if doc.taxes %}
<tr>
<td class="text-left" colspan="1">
{{ _("Net Total") }}
</td>
<td class="text-right totals" colspan="3">
{{ doc.get_formatted("net_total") }}
</td>
</tr>
<div class="w-100 order-taxes mt-5">
<div class="col-4 d-flex border-btm pb-5">
<div class="item-grand-total col-8">
{{ _("Net Total") }}
</div>
<div class="item-grand-total col-4 text-right pr-0">
{{ doc.get_formatted("net_total") }}
</div>
</div>
</div>
{% endif %}
{% for d in doc.taxes %}
{% if d.base_tax_amount %}
<tr>
<td class="text-left" colspan="1">
{{ d.description }}
</td>
<td class="text-right totals" colspan="3">
{{ d.get_formatted("base_tax_amount") }}
</td>
</tr>
<div class="order-taxes w-100 mt-5">
<div class="col-4 d-flex border-btm pb-5">
<div class="item-grand-total col-8">
{{ d.description }}
</div>
<div class="item-grand-total col-4 text-right pr-0">
{{ doc.get_formatted("net_total") }}
</div>
</div>
</div>
{% endif %}
{% endfor %}
{% if doc.doctype == 'Quotation' %}
{% if doc.coupon_code %}
<tr>
<td class="text-left total-discount" colspan="1">
{{ _("Savings") }}
</td>
<td class="text-right tot_quotation_discount total-discount totals" colspan="3">
{% set tot_quotation_discount = [] %}
{%- for item in doc.items -%}
{% if tot_quotation_discount.append((((item.price_list_rate * item.qty)
* item.discount_percentage) / 100)) %}
{% endif %}
{% endfor %}
{{ frappe.utils.fmt_money((tot_quotation_discount | sum),currency=doc.currency) }}
</td>
</tr>
<div class="w-100 mt-5 order-taxes font-weight-bold">
<div class="col-4 d-flex border-btm pb-5">
<div class="item-grand-total col-8">
{{ _("Savings") }}
</div>
<div class="item-grand-total col-4 text-right pr-0">
{% set tot_quotation_discount = [] %}
{%- for item in doc.items -%}
{% if tot_quotation_discount.append((((item.price_list_rate * item.qty)
* item.discount_percentage) / 100)) %}
{% endif %}
{% endfor %}
{{ frappe.utils.fmt_money((tot_quotation_discount | sum),currency=doc.currency) }} </div>
</div>
</div>
{% endif %}
{% endif %}
{% if doc.doctype == 'Sales Order' %}
{% if doc.coupon_code %}
<tr>
<td class="text-left total-discount" colspan="2" style="padding-right: 2rem;">
{{ _("Applied Coupon Code") }}
</td>
<td class="text-right total-discount">
<span>
{%- for row in frappe.get_all(doctype="Coupon Code",
fields=["coupon_code"], filters={ "name":doc.coupon_code}) -%}
<span>{{ row.coupon_code }}</span>
{% endfor %}
</span>
</td>
</tr>
<tr>
<td class="text-left total-discount" colspan="2">
{{ _("Savings") }}
</td>
<td class="text-right total-discount">
<span>
{% set tot_SO_discount = [] %}
{%- for item in doc.items -%}
{% if tot_SO_discount.append((((item.price_list_rate * item.qty)
* item.discount_percentage) / 100)) %}{% endif %}
{% endfor %}
{{ frappe.utils.fmt_money((tot_SO_discount | sum),currency=doc.currency) }}
</span>
</td>
</tr>
<div class="w-100 order-taxes mt-5">
<div class="col-4 d-flex border-btm pb-5">
<div class="item-grand-total col-8">
{{ _("Total Amount") }}
</div>
<div class="item-grand-total col-4 text-right pr-0">
<span>
{% set total_amount = [] %}
{%- for item in doc.items -%}
{% if total_amount.append((item.price_list_rate * item.qty)) %}{% endif %}
{% endfor %}
{{ frappe.utils.fmt_money((total_amount | sum),currency=doc.currency) }}
</span>
</div>
</div>
</div>
<div class="order-taxes w-100 mt-5">
<div class="col-4 d-flex">
<div class="item-grand-total col-8">
{{ _("Applied Coupon Code") }}
</div>
<div class="item-grand-total col-4 text-right pr-0">
<span>
{%- for row in frappe.get_all(doctype="Coupon Code",
fields=["coupon_code"], filters={ "name":doc.coupon_code}) -%}
<span>{{ row.coupon_code }}</span>
{% endfor %}
</span>
</div>
</div>
</div>
<div class="order-taxes mt-5">
<div class="col-4 d-flex border-btm pb-5">
<div class="item-grand-total col-8">
{{ _("Savings") }}
</div>
<div class="item-grand-total col-4 text-right pr-0">
<span>
{% set tot_SO_discount = [] %}
{%- for item in doc.items -%}
{% if tot_SO_discount.append((((item.price_list_rate * item.qty)
* item.discount_percentage) / 100)) %}{% endif %}
{% endfor %}
{{ frappe.utils.fmt_money((tot_SO_discount | sum),currency=doc.currency) }}
</span>
</div>
</div>
</div>
{% endif %}
{% endif %}
<tr>
<th class="text-left item-grand-total" colspan="1">
{{ _("Grand Total") }}
</th>
<th class="text-right item-grand-total totals" colspan="3">
{{ doc.get_formatted("grand_total") }}
</th>
</tr>
<div class="w-100 mt-5 order-taxes font-weight-bold">
<div class="col-4 d-flex">
<div class="item-grand-total col-8">
{{ _("Grand Total") }}
</div>
<div class="item-grand-total col-4 text-right pr-0">
{{ doc.get_formatted("grand_total") }}
</div>
</div>
</div>

View File

@@ -1,20 +1,22 @@
<div class="web-list-item transaction-list-item">
<div class="row">
<div class="row align-items-center">
<div class="col-sm-4">
<span class="indicator small {{ doc.indicator_color or ("blue" if doc.docstatus==1 else "gray") }}">
{{ doc.name }}</span>
<span class="list-item-name font-weight-bold">{{ doc.name }}</span>
<div class="small text-muted transaction-time"
title="{{ frappe.utils.format_datetime(doc.modified, "medium") }}">
{{ frappe.utils.global_date_format(doc.modified) }}
</div>
</div>
<div class="col-sm-5">
<div class="col-sm-3">
<span class="indicator-pill {{ doc.indicator_color or ("blue" if doc.docstatus==1 else "gray") }} list-item-status">{{doc.status}}</span>
</div>
<div class="col-sm-2">
<div class="small text-muted items-preview ellipsis ellipsis-width">
{{ doc.items_preview }}
</div>
</div>
{% if doc.get('grand_total') %}
<div class="col-sm-3 text-right bold">
<div class="col-sm-3 text-right font-weight-bold item-total">
{{ doc.get_formatted("grand_total") }}
</div>
{% endif %}

View File

@@ -5,149 +5,159 @@
{% include "templates/includes/breadcrumbs.html" %}
{% endblock %}
{% block title %}{{ doc.name }}{% endblock %}
{% block title %}
{{ doc.name }}
{% endblock %}
{% block header %}
<h2 class="m-0">{{ doc.name }}</h2>
<h3 class="m-0">{{ doc.name }}</h3>
{% endblock %}
{% block header_actions %}
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
<span class="font-md">{{ _('Actions') }}</span>
<b class="caret"></b>
</button>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
{% if doc.doctype == 'Purchase Order' %}
<a class="dropdown-item" href="/api/method/erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_invoice_from_portal?purchase_order_name={{ doc.name }}" data-action="make_purchase_invoice">{{ _("Make Purchase Invoice") }}</a>
{% endif %}
<a class="dropdown-item" href='/printview?doctype={{ doc.doctype}}&name={{ doc.name }}&format={{ print_format }}'
target="_blank" rel="noopener noreferrer">
{{ _("Print") }}
</a>
</ul>
<div class="row">
<div class="dropdown">
<button class="btn btn-sm btn-secondary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
<span class="font-md">{{ _('Actions') }}</span>
<b class="caret"></b>
</button>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
{% if doc.doctype == 'Purchase Order' and show_make_pi_button %}
<a class="dropdown-item"
href="/api/method/erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_invoice_from_portal?purchase_order_name={{ doc.name }}"
data-action="make_purchase_invoice">{{ _("Make Purchase Invoice") }}
</a>
{% endif %}
<a class="dropdown-item"
href='/printview?doctype={{ doc.doctype}}&name={{ doc.name }}&format={{ print_format }}' target="_blank"
rel="noopener noreferrer">
{{ _("Print") }}
</a>
</ul>
</div>
<div class="form-column col-sm-6">
<div class="page-header-actions-block" data-html-block="header-actions">
<p>
<a href="/api/method/erpnext.accounts.doctype.payment_request.payment_request.make_payment_request?dn={{ doc.name }}&dt={{ doc.doctype }}&submit_doc=1&order_type=Shopping Cart"
class="btn btn-primary btn-sm" id="pay-for-order">
{{ _("Pay") }} {{doc.get_formatted("grand_total") }}
</a>
</p>
</div>
</div>
</div>
{% endblock %}
{% block page_content %}
<div class="row transaction-subheading">
<div class="col-6">
<span class="font-md indicator-pill {{ doc.indicator_color or ("blue" if doc.docstatus==1 else "darkgrey") }}">
{% if doc.doctype == "Quotation" and not doc.docstatus %}
{{ _("Pending") }}
{% else %}
{{ _(doc.get('indicator_title')) or _(doc.status) or _("Submitted") }}
<div>
<div class="row transaction-subheading mt-1">
<div class="col-6 text-muted small mt-1">
{{ frappe.utils.format_date(doc.transaction_date, 'medium') }}
{% if doc.valid_till %}
<p>
{{ _("Valid Till") }}: {{ frappe.utils.format_date(doc.valid_till, 'medium') }}
</p>
{% endif %}
</span>
</div>
</div>
<div class="col-6 text-muted text-right small pt-3">
{{ frappe.utils.format_date(doc.transaction_date, 'medium') }}
{% if doc.valid_till %}
<p>
{{ _("Valid Till") }}: {{ frappe.utils.format_date(doc.valid_till, 'medium') }}
</p>
{% endif %}
</div>
</div>
<p class="small my-3">
{%- set party_name = doc.supplier_name if doc.doctype in ['Supplier Quotation', 'Purchase Invoice', 'Purchase Order'] else doc.customer_name %}
<b>{{ party_name }}</b>
{% if doc.contact_display and doc.contact_display != party_name %}
<br>
{{ doc.contact_display }}
{% endif %}
</p>
{% if doc._header %}
{{ doc._header }}
{% endif %}
<div class="order-container">
<!-- items -->
<table class="order-item-table w-100 table">
<thead class="order-items order-item-header">
<th width="60%">
{{ _("Item") }}
</th>
<th width="20%" class="text-right">
{{ _("Quantity") }}
</th>
<th width="20%" class="text-right">
{{ _("Amount") }}
</th>
</thead>
<tbody>
{% for d in doc.items %}
<tr class="order-items">
<td>
{{ item_name_and_description(d) }}
</td>
<td class="text-right">
{{ d.qty }}
{% if d.delivered_qty is defined and d.delivered_qty != None %}
<p class="text-muted small">{{ _("Delivered") }}&nbsp;{{ d.delivered_qty }}</p>
<div class="row indicator-container mt-2">
<div class="col-10">
<span class="indicator-pill {{ doc.indicator_color or (" blue" if doc.docstatus==1 else "darkgrey" ) }}">
{% if doc.doctype == "Quotation" and not doc.docstatus %}
{{ _("Pending") }}
{% else %}
{{ _(doc.get('indicator_title')) or _(doc.status) or _("Submitted") }}
{% endif %}
</td>
<td class="text-right">
{{ d.get_formatted("amount") }}
<p class="text-muted small">{{ _("Rate:") }}&nbsp;{{ d.get_formatted("rate") }}</p>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- taxes -->
<div class="order-taxes d-flex justify-content-end">
<table>
</span>
</div>
<div class="text-right col-2">
{%- set party_name = doc.supplier_name if doc.doctype in ['Supplier Quotation', 'Purchase Invoice', 'Purchase
Order'] else doc.customer_name %}
<b>{{ party_name }}</b>
{% if doc.contact_display and doc.contact_display != party_name %}
<br>
{{ doc.contact_display }}
{% endif %}
</div>
</div>
{% if doc._header %}
{{ doc._header }}
{% endif %}
<div class="order-container mt-4">
<!-- items -->
<div class="w-100">
<div class="order-items order-item-header mb-1 row text-muted">
<span class="col-5">
{{ _("Item") }}
</span>
<span class="d-s-n col-3">
{{ _("Quantity") }}
</span>
<span class="col-2 pl-10">
{{ _("Rate") }}
</span>
<span class="col-2 text-right">
{{ _("Amount") }}
</span>
</div>
{% for d in doc.items %}
<div class="order-items row align-items-center">
<span class="order-item-name col-5 pr-0">
{{ item_name_and_description(d) }}
</span>
<span class="d-s-n col-3 pl-10">
{{ d.get_formatted("qty") }}
</span>
<span class="order-rate pl-4 col-2">
{{ d.get_formatted("rate") }}
</span>
<span class="col-2 text-right">
{{ d.get_formatted("amount") }}
</span>
</div>
{% endfor %}
</div>
<!-- taxes -->
<div class="">
{% include "erpnext/templates/includes/order/order_taxes.html" %}
</table>
</div>
</div>
</div>
{% if enabled_checkout and ((doc.doctype=="Sales Order" and doc.per_billed <= 0)
or (doc.doctype=="Sales Invoice" and doc.outstanding_amount > 0)) %}
or (doc.doctype=="Sales Invoice" and doc.outstanding_amount> 0)) %}
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="form-column col-sm-6 address-title">
<strong>Payment</strong>
</div>
</div>
</div>
<div class="panel-collapse">
<div class="panel-body text-muted small">
<div class="row">
<div class="form-column col-sm-6">
{% if available_loyalty_points %}
<div class="panel-heading">
<div class="row">
<div class="form-column col-sm-6 address-title">
<strong>Loyalty Points</strong>
</div>
</div>
</div>
<div class="form-group">
<div class="h6">Enter Loyalty Points</div>
<div class="control-input-wrapper">
<div class="control-input">
<input class="form-control" type="number" min="0" max="{{ available_loyalty_points }}" id="loyalty-point-to-redeem">
<input class="form-control" type="number" min="0"
max="{{ available_loyalty_points }}" id="loyalty-point-to-redeem">
</div>
<p class="help-box small text-muted d-none d-sm-block"> Available Points: {{ available_loyalty_points }} </p>
<p class="help-box small text-muted d-none d-sm-block"> Available Points: {{
available_loyalty_points }} </p>
</div>
</div>
{% endif %}
</div>
<div class="form-column col-sm-6">
<div id="loyalty-points-status" style="text-align: right"></div>
<div class="page-header-actions-block" data-html-block="header-actions">
<p class="mt-2" style="float: right;">
<a href="/api/method/erpnext.accounts.doctype.payment_request.payment_request.make_payment_request?dn={{ doc.name }}&dt={{ doc.doctype }}&submit_doc=1&order_type=Shopping Cart"
class="btn btn-primary btn-sm"
id="pay-for-order">
{{ _("Pay") }} {{ doc.get_formatted("grand_total") }}
</a>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -172,17 +182,17 @@
</div>
</div>
{% endif %}
</div>
{% if doc.terms %}
<div class="terms-and-condition text-muted small">
<hr><p>{{ doc.terms }}</p>
<hr>
<p>{{ doc.terms }}</p>
</div>
{% endif %}
{% endblock %}
{% block script %}
<script> {% include "templates/pages/order.js" %} </script>
<script> {% include "templates/pages/order.js" %}</script>
<script>
window.doc_info = {
customer: '{{doc.customer}}',
@@ -192,4 +202,4 @@
currency: '{{ doc.currency }}'
}
</script>
{% endblock %}
{% endblock %}

View File

@@ -52,6 +52,9 @@ def get_context(context):
)
context.available_loyalty_points = int(loyalty_program_details.get("loyalty_points"))
# show Make Purchase Invoice button based on permission
context.show_make_pi_button = frappe.has_permission("Purchase Invoice", "create")
def get_attachments(dt, dn):
return frappe.get_all(

View File

@@ -9,7 +9,6 @@ from frappe import _
def transaction_processing(data, from_doctype, to_doctype):
if isinstance(data, str):
deserialized_data = json.loads(data)
else:
deserialized_data = data
@@ -30,30 +29,29 @@ def transaction_processing(data, from_doctype, to_doctype):
def job(deserialized_data, from_doctype, to_doctype):
failed_history = []
i = 0
fail_count = 0
for d in deserialized_data:
failed = []
try:
i += 1
doc_name = d.get("name")
frappe.db.savepoint("before_creation_state")
task(doc_name, from_doctype, to_doctype)
except Exception as e:
frappe.db.rollback(save_point="before_creation_state")
failed_history.append(e)
failed.append(e)
fail_count += 1
update_logger(
doc_name, e, from_doctype, to_doctype, status="Failed", log_date=str(date.today())
doc_name,
str(frappe.get_traceback()),
from_doctype,
to_doctype,
status="Failed",
log_date=str(date.today()),
)
if not failed:
else:
update_logger(
doc_name, None, from_doctype, to_doctype, status="Success", log_date=str(date.today())
)
show_job_status(failed_history, deserialized_data, to_doctype)
show_job_status(fail_count, len(deserialized_data), to_doctype)
def task(doc_name, from_doctype, to_doctype):
@@ -94,7 +92,7 @@ def task(doc_name, from_doctype, to_doctype):
"Purchase Invoice": purchase_order.make_purchase_invoice,
"Purchase Receipt": purchase_order.make_purchase_receipt,
},
"Purhcase Invoice": {
"Purchase Invoice": {
"Purchase Receipt": purchase_invoice.make_purchase_receipt,
"Payment": payment_entry.get_payment_entry,
},
@@ -150,15 +148,14 @@ def update_logger(doc_name, e, from_doctype, to_doctype, status, log_date=None,
log_doc.save()
def show_job_status(failed_history, deserialized_data, to_doctype):
if not failed_history:
def show_job_status(fail_count, deserialized_data_count, to_doctype):
if not fail_count:
frappe.msgprint(
_("Creation of {0} successful").format(to_doctype),
title="Successful",
indicator="green",
)
if len(failed_history) != 0 and len(failed_history) < len(deserialized_data):
elif fail_count != 0 and fail_count < deserialized_data_count:
frappe.msgprint(
_(
"""Creation of {0} partially successful.
@@ -167,8 +164,7 @@ def show_job_status(failed_history, deserialized_data, to_doctype):
title="Partially successful",
indicator="orange",
)
if len(failed_history) == len(deserialized_data):
else:
frappe.msgprint(
_(
"""Creation of {0} failed.
@@ -180,9 +176,7 @@ def show_job_status(failed_history, deserialized_data, to_doctype):
def record_exists(log_doc, doc_name, status):
record = mark_retrired_transaction(log_doc, doc_name)
if record and status == "Failed":
return False
elif record and status == "Success":

View File

@@ -45,7 +45,7 @@
.time-slot.selected {
color: white;
background: #5e64ff;
background: var(--primary-color);
}
.time-slot.selected .text-muted {